mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Remote clusters] Cloud deployment form when adding new cluster (#94450)
* Implemented in-form Cloud deployment url input * Fixed i18n files and added mode switch back for non-Cloud * Added cloud docs link to the documentation service, fixed snapshot tests * Fixed docs build * Added jest test for the new cloud url input * Added unit test for cloud validation * Fixed eslint error * Fixed ts errors * Added ts-ignore * Fixed ts errors * Refactored connection mode component and component tests * Fixed import * Fixed copy * Fixed copy * Reverted docs changes * Reverted docs changes * Replaced the screenshot with a popover and refactored integration tests * Added todo for cloud deployments link * Changed cloud URL help text copy * Added cloud base url and deleted unnecessary base path * Fixed es error * Fixed es error * Changed wording * Reverted docs changes * Updated the help popover * Deleted unneeded fragment component * Deleted unneeded fragment component * Updated tests descriptions to be more detailed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
03a51f4eec
commit
0316787ead
45 changed files with 2184 additions and 3503 deletions
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { registerTestBed } from '@kbn/test/jest';
|
|
||||||
|
|
||||||
import { RemoteClusterAdd } from '../../../public/application/sections/remote_cluster_add';
|
|
||||||
import { createRemoteClustersStore } from '../../../public/application/store';
|
|
||||||
import { registerRouter } from '../../../public/application/services/routing';
|
|
||||||
|
|
||||||
const testBedConfig = {
|
|
||||||
store: createRemoteClustersStore,
|
|
||||||
memoryRouter: {
|
|
||||||
onRouter: (router) => registerRouter(router),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTestBed = registerTestBed(RemoteClusterAdd, testBedConfig);
|
|
||||||
|
|
||||||
export const setup = (props) => {
|
|
||||||
const testBed = initTestBed(props);
|
|
||||||
|
|
||||||
// User actions
|
|
||||||
const clickSaveForm = async () => {
|
|
||||||
await act(async () => {
|
|
||||||
testBed.find('remoteClusterFormSaveButton').simulate('click');
|
|
||||||
});
|
|
||||||
|
|
||||||
testBed.component.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...testBed,
|
|
||||||
actions: {
|
|
||||||
clickSaveForm,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { registerTestBed } from '@kbn/test/jest';
|
||||||
|
|
||||||
|
import { RemoteClusterAdd } from '../../../public/application/sections';
|
||||||
|
import { createRemoteClustersStore } from '../../../public/application/store';
|
||||||
|
import { AppRouter, registerRouter } from '../../../public/application/services';
|
||||||
|
import { createRemoteClustersActions } from '../helpers';
|
||||||
|
import { AppContextProvider } from '../../../public/application/app_context';
|
||||||
|
|
||||||
|
const ComponentWithContext = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => {
|
||||||
|
return (
|
||||||
|
<AppContextProvider context={{ isCloudEnabled, cloudBaseUrl: 'test.com' }}>
|
||||||
|
<RemoteClusterAdd />
|
||||||
|
</AppContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBedConfig = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => {
|
||||||
|
return {
|
||||||
|
store: createRemoteClustersStore,
|
||||||
|
memoryRouter: {
|
||||||
|
onRouter: (router: AppRouter) => registerRouter(router),
|
||||||
|
},
|
||||||
|
defaultProps: { isCloudEnabled },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTestBed = (isCloudEnabled: boolean) =>
|
||||||
|
registerTestBed(ComponentWithContext, testBedConfig({ isCloudEnabled }))();
|
||||||
|
|
||||||
|
export const setup = async (isCloudEnabled = false) => {
|
||||||
|
const testBed = await initTestBed(isCloudEnabled);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...testBed,
|
||||||
|
actions: {
|
||||||
|
...createRemoteClustersActions(testBed),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,230 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { setupEnvironment } from '../helpers';
|
|
||||||
import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters';
|
|
||||||
import { setup } from './remote_clusters_add.helpers';
|
|
||||||
|
|
||||||
describe('Create Remote cluster', () => {
|
|
||||||
describe('on component mount', () => {
|
|
||||||
let find;
|
|
||||||
let exists;
|
|
||||||
let actions;
|
|
||||||
let form;
|
|
||||||
let server;
|
|
||||||
let component;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
({ server } = setupEnvironment());
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
server.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await act(async () => {
|
|
||||||
({ form, exists, find, actions, component } = setup());
|
|
||||||
});
|
|
||||||
component.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have the title of the page set correctly', () => {
|
|
||||||
expect(exists('remoteClusterPageTitle')).toBe(true);
|
|
||||||
expect(find('remoteClusterPageTitle').text()).toEqual('Add remote cluster');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have a link to the documentation', () => {
|
|
||||||
expect(exists('remoteClusterDocsButton')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have a toggle to Skip unavailable remote cluster', () => {
|
|
||||||
expect(exists('remoteClusterFormSkipUnavailableFormToggle')).toBe(true);
|
|
||||||
|
|
||||||
// By default it should be set to "false"
|
|
||||||
expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
form.toggleEuiSwitch('remoteClusterFormSkipUnavailableFormToggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
|
|
||||||
expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have a toggle to enable "proxy" mode for a remote cluster', () => {
|
|
||||||
expect(exists('remoteClusterFormConnectionModeToggle')).toBe(true);
|
|
||||||
|
|
||||||
// By default it should be set to "false"
|
|
||||||
expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
|
|
||||||
expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display errors and disable the save button when clicking "save" without filling the form', async () => {
|
|
||||||
expect(exists('remoteClusterFormGlobalError')).toBe(false);
|
|
||||||
expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false);
|
|
||||||
|
|
||||||
await actions.clickSaveForm();
|
|
||||||
|
|
||||||
expect(exists('remoteClusterFormGlobalError')).toBe(true);
|
|
||||||
expect(form.getErrorsMessages()).toEqual([
|
|
||||||
'Name is required.',
|
|
||||||
'At least one seed node is required.',
|
|
||||||
]);
|
|
||||||
expect(find('remoteClusterFormSaveButton').props().disabled).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('form validation', () => {
|
|
||||||
describe('remote cluster name', () => {
|
|
||||||
let component;
|
|
||||||
let actions;
|
|
||||||
let form;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await act(async () => {
|
|
||||||
({ component, form, actions } = setup());
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not allow spaces', async () => {
|
|
||||||
form.setInputValue('remoteClusterFormNameInput', 'with space');
|
|
||||||
|
|
||||||
await actions.clickSaveForm();
|
|
||||||
|
|
||||||
expect(form.getErrorsMessages()).toContain('Spaces are not allowed in the name.');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => {
|
|
||||||
const expectInvalidChar = (char) => {
|
|
||||||
if (char === '-' || char === '_') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
form.setInputValue('remoteClusterFormNameInput', `with${char}`);
|
|
||||||
|
|
||||||
expect(form.getErrorsMessages()).toContain(
|
|
||||||
`Remove the character ${char} from the name.`
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
throw Error(`Char "${char}" expected invalid but was allowed`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await actions.clickSaveForm(); // display form errors
|
|
||||||
|
|
||||||
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('seeds', () => {
|
|
||||||
let actions;
|
|
||||||
let form;
|
|
||||||
let component;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await act(async () => {
|
|
||||||
({ form, actions, component } = setup());
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
|
|
||||||
form.setInputValue('remoteClusterFormNameInput', 'remote_cluster_test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => {
|
|
||||||
await actions.clickSaveForm(); // display form errors
|
|
||||||
|
|
||||||
const notInArray = (array) => (value) => array.indexOf(value) < 0;
|
|
||||||
|
|
||||||
const expectInvalidChar = (char) => {
|
|
||||||
form.setComboBoxValue('remoteClusterFormSeedsInput', `192.16${char}:3000`);
|
|
||||||
expect(form.getErrorsMessages()).toContain(
|
|
||||||
`Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS]
|
|
||||||
.filter(notInArray(['-', '_', ':']))
|
|
||||||
.forEach(expectInvalidChar);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should require a numeric "port" to be set', async () => {
|
|
||||||
await actions.clickSaveForm();
|
|
||||||
|
|
||||||
form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1');
|
|
||||||
expect(form.getErrorsMessages()).toContain('A port is required.');
|
|
||||||
|
|
||||||
form.setComboBoxValue('remoteClusterFormSeedsInput', '192.168.1.1:abc');
|
|
||||||
expect(form.getErrorsMessages()).toContain('A port is required.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('proxy address', () => {
|
|
||||||
let actions;
|
|
||||||
let form;
|
|
||||||
let component;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await act(async () => {
|
|
||||||
({ form, actions, component } = setup());
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
// Enable "proxy" mode
|
|
||||||
form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle');
|
|
||||||
});
|
|
||||||
|
|
||||||
component.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => {
|
|
||||||
await actions.clickSaveForm(); // display form errors
|
|
||||||
|
|
||||||
const notInArray = (array) => (value) => array.indexOf(value) < 0;
|
|
||||||
|
|
||||||
const expectInvalidChar = (char) => {
|
|
||||||
form.setInputValue('remoteClusterFormProxyAddressInput', `192.16${char}:3000`);
|
|
||||||
expect(form.getErrorsMessages()).toContain(
|
|
||||||
'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS]
|
|
||||||
.filter(notInArray(['-', '_', ':']))
|
|
||||||
.forEach(expectInvalidChar);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should require a numeric "port" to be set', async () => {
|
|
||||||
await actions.clickSaveForm();
|
|
||||||
|
|
||||||
form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1');
|
|
||||||
expect(form.getErrorsMessages()).toContain('A port is required.');
|
|
||||||
|
|
||||||
form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1:abc');
|
|
||||||
expect(form.getErrorsMessages()).toContain('A port is required.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,260 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SinonFakeServer } from 'sinon';
|
||||||
|
import { TestBed } from '@kbn/test/jest';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import { setupEnvironment, RemoteClustersActions } from '../helpers';
|
||||||
|
import { setup } from './remote_clusters_add.helpers';
|
||||||
|
import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters';
|
||||||
|
|
||||||
|
const notInArray = (array: string[]) => (value: string) => array.indexOf(value) < 0;
|
||||||
|
|
||||||
|
let component: TestBed['component'];
|
||||||
|
let actions: RemoteClustersActions;
|
||||||
|
let server: SinonFakeServer;
|
||||||
|
|
||||||
|
describe('Create Remote cluster', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
({ server } = setupEnvironment());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ actions, component } = await setup());
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on component mount', () => {
|
||||||
|
test('should have the title of the page set correctly', () => {
|
||||||
|
expect(actions.pageTitle.exists()).toBe(true);
|
||||||
|
expect(actions.pageTitle.text()).toEqual('Add remote cluster');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have a link to the documentation', () => {
|
||||||
|
expect(actions.docsButtonExists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have a toggle to Skip unavailable remote cluster', () => {
|
||||||
|
expect(actions.skipUnavailableSwitch.exists()).toBe(true);
|
||||||
|
|
||||||
|
// By default it should be set to "false"
|
||||||
|
expect(actions.skipUnavailableSwitch.isChecked()).toBe(false);
|
||||||
|
|
||||||
|
actions.skipUnavailableSwitch.toggle();
|
||||||
|
|
||||||
|
expect(actions.skipUnavailableSwitch.isChecked()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on prem', () => {
|
||||||
|
test('should have a toggle to enable "proxy" mode for a remote cluster', () => {
|
||||||
|
expect(actions.connectionModeSwitch.exists()).toBe(true);
|
||||||
|
|
||||||
|
// By default it should be set to "false"
|
||||||
|
expect(actions.connectionModeSwitch.isChecked()).toBe(false);
|
||||||
|
|
||||||
|
actions.connectionModeSwitch.toggle();
|
||||||
|
|
||||||
|
expect(actions.connectionModeSwitch.isChecked()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server name has optional label', () => {
|
||||||
|
actions.connectionModeSwitch.toggle();
|
||||||
|
expect(actions.serverNameInput.getLabel()).toBe('Server name (optional)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display errors and disable the save button when clicking "save" without filling the form', async () => {
|
||||||
|
expect(actions.globalErrorExists()).toBe(false);
|
||||||
|
expect(actions.saveButton.isDisabled()).toBe(false);
|
||||||
|
|
||||||
|
await actions.saveButton.click();
|
||||||
|
|
||||||
|
expect(actions.globalErrorExists()).toBe(true);
|
||||||
|
expect(actions.getErrorMessages()).toEqual([
|
||||||
|
'Name is required.',
|
||||||
|
// seeds input is switched on by default on prem and is required
|
||||||
|
'At least one seed node is required.',
|
||||||
|
]);
|
||||||
|
expect(actions.saveButton.isDisabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders no switch for cloud url input and proxy address + server name input modes', () => {
|
||||||
|
expect(actions.cloudUrlSwitch.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('on cloud', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ actions, component } = await setup(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a switch between cloud url input and proxy address + server name input for proxy connection', () => {
|
||||||
|
expect(actions.cloudUrlSwitch.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders no switch between sniff and proxy modes', () => {
|
||||||
|
expect(actions.connectionModeSwitch.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
test('defaults to cloud url input for proxy connection', () => {
|
||||||
|
expect(actions.cloudUrlSwitch.isChecked()).toBe(false);
|
||||||
|
});
|
||||||
|
test('server name has no optional label', () => {
|
||||||
|
actions.cloudUrlSwitch.toggle();
|
||||||
|
expect(actions.serverNameInput.getLabel()).toBe('Server name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('form validation', () => {
|
||||||
|
describe('remote cluster name', () => {
|
||||||
|
test('should not allow spaces', async () => {
|
||||||
|
actions.nameInput.setValue('with space');
|
||||||
|
|
||||||
|
await actions.saveButton.click();
|
||||||
|
|
||||||
|
expect(actions.getErrorMessages()).toContain('Spaces are not allowed in the name.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should only allow alpha-numeric characters, "-" (dash) and "_" (underscore)', async () => {
|
||||||
|
const expectInvalidChar = (char: string) => {
|
||||||
|
if (char === '-' || char === '_') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
actions.nameInput.setValue(`with${char}`);
|
||||||
|
|
||||||
|
expect(actions.getErrorMessages()).toContain(
|
||||||
|
`Remove the character ${char} from the name.`
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
throw Error(`Char "${char}" expected invalid but was allowed`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await actions.saveButton.click(); // display form errors
|
||||||
|
|
||||||
|
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('proxy address', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ actions, component } = await setup());
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
actions.connectionModeSwitch.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => {
|
||||||
|
await actions.saveButton.click(); // display form errors
|
||||||
|
|
||||||
|
const expectInvalidChar = (char: string) => {
|
||||||
|
actions.proxyAddressInput.setValue(`192.16${char}:3000`);
|
||||||
|
expect(actions.getErrorMessages()).toContain(
|
||||||
|
'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS]
|
||||||
|
.filter(notInArray(['-', '_', ':']))
|
||||||
|
.forEach(expectInvalidChar);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require a numeric "port" to be set', async () => {
|
||||||
|
await actions.saveButton.click();
|
||||||
|
|
||||||
|
actions.proxyAddressInput.setValue('192.168.1.1');
|
||||||
|
expect(actions.getErrorMessages()).toContain('A port is required.');
|
||||||
|
|
||||||
|
actions.proxyAddressInput.setValue('192.168.1.1:abc');
|
||||||
|
expect(actions.getErrorMessages()).toContain('A port is required.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on prem', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ actions, component } = await setup());
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
actions.nameInput.setValue('remote_cluster_test');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('seeds', () => {
|
||||||
|
test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => {
|
||||||
|
await actions.saveButton.click(); // display form errors
|
||||||
|
|
||||||
|
const expectInvalidChar = (char: string) => {
|
||||||
|
actions.seedsInput.setValue(`192.16${char}:3000`);
|
||||||
|
expect(actions.getErrorMessages()).toContain(
|
||||||
|
`Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS]
|
||||||
|
.filter(notInArray(['-', '_', ':']))
|
||||||
|
.forEach(expectInvalidChar);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require a numeric "port" to be set', async () => {
|
||||||
|
await actions.saveButton.click();
|
||||||
|
|
||||||
|
actions.seedsInput.setValue('192.168.1.1');
|
||||||
|
expect(actions.getErrorMessages()).toContain('A port is required.');
|
||||||
|
|
||||||
|
actions.seedsInput.setValue('192.168.1.1:abc');
|
||||||
|
expect(actions.getErrorMessages()).toContain('A port is required.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server name is optional (proxy connection)', () => {
|
||||||
|
actions.connectionModeSwitch.toggle();
|
||||||
|
actions.saveButton.click();
|
||||||
|
expect(actions.getErrorMessages()).toEqual(['A proxy address is required.']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on cloud', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ actions, component } = await setup(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cloud url is required since cloud url input is enabled by default', () => {
|
||||||
|
actions.saveButton.click();
|
||||||
|
expect(actions.getErrorMessages()).toContain('A url is required.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proxy address and server name are required when cloud url input is disabled', () => {
|
||||||
|
actions.cloudUrlSwitch.toggle();
|
||||||
|
actions.saveButton.click();
|
||||||
|
expect(actions.getErrorMessages()).toEqual([
|
||||||
|
'Name is required.',
|
||||||
|
'A proxy address is required.',
|
||||||
|
'A server name is required.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { registerTestBed } from '@kbn/test/jest';
|
|
||||||
|
|
||||||
import { RemoteClusterEdit } from '../../../public/application/sections/remote_cluster_edit';
|
|
||||||
import { createRemoteClustersStore } from '../../../public/application/store';
|
|
||||||
import { registerRouter } from '../../../public/application/services/routing';
|
|
||||||
|
|
||||||
export const REMOTE_CLUSTER_EDIT_NAME = 'new-york';
|
|
||||||
|
|
||||||
export const REMOTE_CLUSTER_EDIT = {
|
|
||||||
name: REMOTE_CLUSTER_EDIT_NAME,
|
|
||||||
seeds: ['localhost:9400'],
|
|
||||||
skipUnavailable: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const testBedConfig = {
|
|
||||||
store: createRemoteClustersStore,
|
|
||||||
memoryRouter: {
|
|
||||||
onRouter: (router) => registerRouter(router),
|
|
||||||
// The remote cluster name to edit is read from the router ":id" param
|
|
||||||
// so we first set it in our initial entries
|
|
||||||
initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`],
|
|
||||||
// and then we declarae the :id param on the component route path
|
|
||||||
componentRoutePath: '/:name',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setup = registerTestBed(RemoteClusterEdit, testBedConfig);
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { registerTestBed, TestBedConfig } from '@kbn/test/jest';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { RemoteClusterEdit } from '../../../public/application/sections';
|
||||||
|
import { createRemoteClustersStore } from '../../../public/application/store';
|
||||||
|
import { AppRouter, registerRouter } from '../../../public/application/services';
|
||||||
|
import { createRemoteClustersActions } from '../helpers';
|
||||||
|
import { AppContextProvider } from '../../../public/application/app_context';
|
||||||
|
|
||||||
|
export const REMOTE_CLUSTER_EDIT_NAME = 'new-york';
|
||||||
|
|
||||||
|
export const REMOTE_CLUSTER_EDIT = {
|
||||||
|
name: REMOTE_CLUSTER_EDIT_NAME,
|
||||||
|
seeds: ['localhost:9400'],
|
||||||
|
skipUnavailable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComponentWithContext = (props: { isCloudEnabled: boolean }) => {
|
||||||
|
const { isCloudEnabled, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<AppContextProvider context={{ isCloudEnabled, cloudBaseUrl: 'test.com' }}>
|
||||||
|
<RemoteClusterEdit {...rest} />
|
||||||
|
</AppContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBedConfig: TestBedConfig = {
|
||||||
|
store: createRemoteClustersStore,
|
||||||
|
memoryRouter: {
|
||||||
|
onRouter: (router: AppRouter) => registerRouter(router),
|
||||||
|
// The remote cluster name to edit is read from the router ":id" param
|
||||||
|
// so we first set it in our initial entries
|
||||||
|
initialEntries: [`/${REMOTE_CLUSTER_EDIT_NAME}`],
|
||||||
|
// and then we declare the :id param on the component route path
|
||||||
|
componentRoutePath: '/:name',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTestBed = (isCloudEnabled: boolean) =>
|
||||||
|
registerTestBed(ComponentWithContext, testBedConfig)({ isCloudEnabled });
|
||||||
|
|
||||||
|
export const setup = async (isCloudEnabled = false) => {
|
||||||
|
const testBed = await initTestBed(isCloudEnabled);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...testBed,
|
||||||
|
actions: {
|
||||||
|
...createRemoteClustersActions(testBed),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form';
|
|
||||||
import { setupEnvironment } from '../helpers';
|
|
||||||
import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers';
|
|
||||||
import {
|
|
||||||
setup,
|
|
||||||
REMOTE_CLUSTER_EDIT,
|
|
||||||
REMOTE_CLUSTER_EDIT_NAME,
|
|
||||||
} from './remote_clusters_edit.helpers';
|
|
||||||
|
|
||||||
describe('Edit Remote cluster', () => {
|
|
||||||
let component;
|
|
||||||
let find;
|
|
||||||
let exists;
|
|
||||||
|
|
||||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
server.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]);
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await act(async () => {
|
|
||||||
({ component, find, exists } = setup());
|
|
||||||
});
|
|
||||||
component.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have the title of the page set correctly', () => {
|
|
||||||
expect(exists('remoteClusterPageTitle')).toBe(true);
|
|
||||||
expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should have a link to the documentation', () => {
|
|
||||||
expect(exists('remoteClusterDocsButton')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* As the "edit" remote cluster component uses the same form underneath that
|
|
||||||
* the "create" remote cluster, we won't test it again but simply make sure that
|
|
||||||
* the form component is indeed shared between the 2 app sections.
|
|
||||||
*/
|
|
||||||
test('should use the same Form component as the "<RemoteClusterAdd />" component', async () => {
|
|
||||||
let addRemoteClusterTestBed;
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
addRemoteClusterTestBed = setupRemoteClustersAdd();
|
|
||||||
});
|
|
||||||
|
|
||||||
addRemoteClusterTestBed.component.update();
|
|
||||||
|
|
||||||
const formEdit = component.find(RemoteClusterForm);
|
|
||||||
const formAdd = addRemoteClusterTestBed.component.find(RemoteClusterForm);
|
|
||||||
|
|
||||||
expect(formEdit.length).toBe(1);
|
|
||||||
expect(formAdd.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should populate the form fields with the values from the remote cluster loaded', () => {
|
|
||||||
expect(find('remoteClusterFormNameInput').props().value).toBe(REMOTE_CLUSTER_EDIT_NAME);
|
|
||||||
expect(find('remoteClusterFormSeedsInput').text()).toBe(REMOTE_CLUSTER_EDIT.seeds.join(''));
|
|
||||||
expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(
|
|
||||||
REMOTE_CLUSTER_EDIT.skipUnavailable
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should disable the form name input', () => {
|
|
||||||
expect(find('remoteClusterFormNameInput').props().disabled).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { TestBed } from '@kbn/test/jest';
|
||||||
|
|
||||||
|
import { RemoteClusterForm } from '../../../public/application/sections/components/remote_cluster_form';
|
||||||
|
import { RemoteClustersActions, setupEnvironment } from '../helpers';
|
||||||
|
import { setup as setupRemoteClustersAdd } from '../add/remote_clusters_add.helpers';
|
||||||
|
import {
|
||||||
|
setup,
|
||||||
|
REMOTE_CLUSTER_EDIT,
|
||||||
|
REMOTE_CLUSTER_EDIT_NAME,
|
||||||
|
} from './remote_clusters_edit.helpers';
|
||||||
|
import { Cluster } from '../../../common/lib';
|
||||||
|
|
||||||
|
let component: TestBed['component'];
|
||||||
|
let actions: RemoteClustersActions;
|
||||||
|
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||||
|
|
||||||
|
describe('Edit Remote cluster', () => {
|
||||||
|
afterAll(() => {
|
||||||
|
server.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
httpRequestsMockHelpers.setLoadRemoteClustersResponse([REMOTE_CLUSTER_EDIT]);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
({ component, actions } = await setup());
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have the title of the page set correctly', () => {
|
||||||
|
expect(actions.pageTitle.exists()).toBe(true);
|
||||||
|
expect(actions.pageTitle.text()).toEqual('Edit remote cluster');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have a link to the documentation', () => {
|
||||||
|
expect(actions.docsButtonExists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As the "edit" remote cluster component uses the same form underneath that
|
||||||
|
* the "create" remote cluster, we won't test it again but simply make sure that
|
||||||
|
* the form component is indeed shared between the 2 app sections.
|
||||||
|
*/
|
||||||
|
test('should use the same Form component as the "<RemoteClusterAdd />" component', async () => {
|
||||||
|
let addRemoteClusterTestBed: TestBed;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
addRemoteClusterTestBed = await setupRemoteClustersAdd();
|
||||||
|
});
|
||||||
|
|
||||||
|
addRemoteClusterTestBed!.component.update();
|
||||||
|
|
||||||
|
const formEdit = component.find(RemoteClusterForm);
|
||||||
|
const formAdd = addRemoteClusterTestBed!.component.find(RemoteClusterForm);
|
||||||
|
|
||||||
|
expect(formEdit.length).toBe(1);
|
||||||
|
expect(formAdd.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should populate the form fields with the values from the remote cluster loaded', () => {
|
||||||
|
expect(actions.nameInput.getValue()).toBe(REMOTE_CLUSTER_EDIT_NAME);
|
||||||
|
// seeds input for sniff connection is not shown on Cloud
|
||||||
|
expect(actions.seedsInput.getValue()).toBe(REMOTE_CLUSTER_EDIT.seeds.join(''));
|
||||||
|
expect(actions.skipUnavailableSwitch.isChecked()).toBe(REMOTE_CLUSTER_EDIT.skipUnavailable);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable the form name input', () => {
|
||||||
|
expect(actions.nameInput.isDisabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on cloud', () => {
|
||||||
|
const cloudUrl = 'cloud-url';
|
||||||
|
const defaultCloudPort = '9400';
|
||||||
|
test('existing cluster that defaults to cloud url (default port)', async () => {
|
||||||
|
const cluster: Cluster = {
|
||||||
|
name: REMOTE_CLUSTER_EDIT_NAME,
|
||||||
|
mode: 'proxy',
|
||||||
|
proxyAddress: `${cloudUrl}:${defaultCloudPort}`,
|
||||||
|
serverName: cloudUrl,
|
||||||
|
};
|
||||||
|
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
({ component, actions } = await setup(true));
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
expect(actions.cloudUrlInput.exists()).toBe(true);
|
||||||
|
expect(actions.cloudUrlInput.getValue()).toBe(cloudUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('existing cluster that defaults to manual input (non-default port)', async () => {
|
||||||
|
const cluster: Cluster = {
|
||||||
|
name: REMOTE_CLUSTER_EDIT_NAME,
|
||||||
|
mode: 'proxy',
|
||||||
|
proxyAddress: `${cloudUrl}:9500`,
|
||||||
|
serverName: cloudUrl,
|
||||||
|
};
|
||||||
|
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
({ component, actions } = await setup(true));
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
expect(actions.cloudUrlInput.exists()).toBe(false);
|
||||||
|
|
||||||
|
expect(actions.proxyAddressInput.exists()).toBe(true);
|
||||||
|
expect(actions.serverNameInput.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('existing cluster that defaults to manual input (proxy address is different from server name)', async () => {
|
||||||
|
const cluster: Cluster = {
|
||||||
|
name: REMOTE_CLUSTER_EDIT_NAME,
|
||||||
|
mode: 'proxy',
|
||||||
|
proxyAddress: `${cloudUrl}:${defaultCloudPort}`,
|
||||||
|
serverName: 'another-value',
|
||||||
|
};
|
||||||
|
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
({ component, actions } = await setup(true));
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
|
||||||
|
expect(actions.cloudUrlInput.exists()).toBe(false);
|
||||||
|
|
||||||
|
expect(actions.proxyAddressInput.exists()).toBe(true);
|
||||||
|
expect(actions.serverNameInput.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,25 +5,24 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon, { SinonFakeServer } from 'sinon';
|
||||||
|
import { Cluster } from '../../../common/lib';
|
||||||
|
|
||||||
// Register helpers to mock HTTP Requests
|
// Register helpers to mock HTTP Requests
|
||||||
const registerHttpRequestMockHelpers = (server) => {
|
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||||
const mockResponse = (response) => [
|
const mockResponse = (response: Cluster[] | { itemsDeleted: string[]; errors: string[] }) => [
|
||||||
200,
|
200,
|
||||||
{ 'Content-Type': 'application/json' },
|
{ 'Content-Type': 'application/json' },
|
||||||
JSON.stringify(response),
|
JSON.stringify(response),
|
||||||
];
|
];
|
||||||
|
|
||||||
const setLoadRemoteClustersResponse = (response) => {
|
const setLoadRemoteClustersResponse = (response: Cluster[] = []) => {
|
||||||
server.respondWith('GET', '/api/remote_clusters', [
|
server.respondWith('GET', '/api/remote_clusters', mockResponse(response));
|
||||||
200,
|
|
||||||
{ 'Content-Type': 'application/json' },
|
|
||||||
JSON.stringify(response),
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setDeleteRemoteClusterResponse = (response) => {
|
const setDeleteRemoteClusterResponse = (
|
||||||
|
response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] }
|
||||||
|
) => {
|
||||||
server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response));
|
server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response));
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,3 +7,4 @@
|
||||||
|
|
||||||
export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest';
|
export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest';
|
||||||
export { setupEnvironment } from './setup_environment';
|
export { setupEnvironment } from './setup_environment';
|
||||||
|
export { createRemoteClustersActions, RemoteClustersActions } from './remote_clusters_actions';
|
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TestBed } from '@kbn/test/jest';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
export interface RemoteClustersActions {
|
||||||
|
docsButtonExists: () => boolean;
|
||||||
|
pageTitle: {
|
||||||
|
exists: () => boolean;
|
||||||
|
text: () => string;
|
||||||
|
};
|
||||||
|
nameInput: {
|
||||||
|
setValue: (name: string) => void;
|
||||||
|
getValue: () => string;
|
||||||
|
isDisabled: () => boolean;
|
||||||
|
};
|
||||||
|
skipUnavailableSwitch: {
|
||||||
|
exists: () => boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
isChecked: () => boolean;
|
||||||
|
};
|
||||||
|
connectionModeSwitch: {
|
||||||
|
exists: () => boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
isChecked: () => boolean;
|
||||||
|
};
|
||||||
|
cloudUrlSwitch: {
|
||||||
|
toggle: () => void;
|
||||||
|
exists: () => boolean;
|
||||||
|
isChecked: () => boolean;
|
||||||
|
};
|
||||||
|
cloudUrlInput: {
|
||||||
|
exists: () => boolean;
|
||||||
|
getValue: () => string;
|
||||||
|
};
|
||||||
|
seedsInput: {
|
||||||
|
setValue: (seed: string) => void;
|
||||||
|
getValue: () => string;
|
||||||
|
};
|
||||||
|
proxyAddressInput: {
|
||||||
|
setValue: (proxyAddress: string) => void;
|
||||||
|
exists: () => boolean;
|
||||||
|
};
|
||||||
|
serverNameInput: {
|
||||||
|
getLabel: () => string;
|
||||||
|
exists: () => boolean;
|
||||||
|
};
|
||||||
|
saveButton: {
|
||||||
|
click: () => void;
|
||||||
|
isDisabled: () => boolean;
|
||||||
|
};
|
||||||
|
getErrorMessages: () => string[];
|
||||||
|
globalErrorExists: () => boolean;
|
||||||
|
}
|
||||||
|
export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersActions => {
|
||||||
|
const { form, exists, find, component } = testBed;
|
||||||
|
|
||||||
|
const docsButtonExists = () => exists('remoteClusterDocsButton');
|
||||||
|
const createPageTitleActions = () => {
|
||||||
|
const pageTitleSelector = 'remoteClusterPageTitle';
|
||||||
|
return {
|
||||||
|
pageTitle: {
|
||||||
|
exists: () => exists(pageTitleSelector),
|
||||||
|
text: () => find(pageTitleSelector).text(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const createNameInputActions = () => {
|
||||||
|
const nameInputSelector = 'remoteClusterFormNameInput';
|
||||||
|
return {
|
||||||
|
nameInput: {
|
||||||
|
setValue: (name: string) => form.setInputValue(nameInputSelector, name),
|
||||||
|
getValue: () => find(nameInputSelector).props().value,
|
||||||
|
isDisabled: () => find(nameInputSelector).props().disabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSkipUnavailableActions = () => {
|
||||||
|
const skipUnavailableToggleSelector = 'remoteClusterFormSkipUnavailableFormToggle';
|
||||||
|
return {
|
||||||
|
skipUnavailableSwitch: {
|
||||||
|
exists: () => exists(skipUnavailableToggleSelector),
|
||||||
|
toggle: () => {
|
||||||
|
act(() => {
|
||||||
|
form.toggleEuiSwitch(skipUnavailableToggleSelector);
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
},
|
||||||
|
isChecked: () => find(skipUnavailableToggleSelector).props()['aria-checked'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createConnectionModeActions = () => {
|
||||||
|
const connectionModeToggleSelector = 'remoteClusterFormConnectionModeToggle';
|
||||||
|
return {
|
||||||
|
connectionModeSwitch: {
|
||||||
|
exists: () => exists(connectionModeToggleSelector),
|
||||||
|
toggle: () => {
|
||||||
|
act(() => {
|
||||||
|
form.toggleEuiSwitch(connectionModeToggleSelector);
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
},
|
||||||
|
isChecked: () => find(connectionModeToggleSelector).props()['aria-checked'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCloudUrlSwitchActions = () => {
|
||||||
|
const cloudUrlSelector = 'remoteClusterFormCloudUrlToggle';
|
||||||
|
return {
|
||||||
|
cloudUrlSwitch: {
|
||||||
|
exists: () => exists(cloudUrlSelector),
|
||||||
|
toggle: () => {
|
||||||
|
act(() => {
|
||||||
|
form.toggleEuiSwitch(cloudUrlSelector);
|
||||||
|
});
|
||||||
|
component.update();
|
||||||
|
},
|
||||||
|
isChecked: () => find(cloudUrlSelector).props()['aria-checked'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSeedsInputActions = () => {
|
||||||
|
const seedsInputSelector = 'remoteClusterFormSeedsInput';
|
||||||
|
return {
|
||||||
|
seedsInput: {
|
||||||
|
setValue: (seed: string) => form.setComboBoxValue(seedsInputSelector, seed),
|
||||||
|
getValue: () => find(seedsInputSelector).text(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createProxyAddressActions = () => {
|
||||||
|
const proxyAddressSelector = 'remoteClusterFormProxyAddressInput';
|
||||||
|
return {
|
||||||
|
proxyAddressInput: {
|
||||||
|
setValue: (proxyAddress: string) => form.setInputValue(proxyAddressSelector, proxyAddress),
|
||||||
|
exists: () => exists(proxyAddressSelector),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSaveButtonActions = () => {
|
||||||
|
const click = () => {
|
||||||
|
act(() => {
|
||||||
|
find('remoteClusterFormSaveButton').simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
component.update();
|
||||||
|
};
|
||||||
|
const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled;
|
||||||
|
return { saveButton: { click, isDisabled } };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createServerNameActions = () => {
|
||||||
|
const serverNameSelector = 'remoteClusterFormServerNameFormRow';
|
||||||
|
return {
|
||||||
|
serverNameInput: {
|
||||||
|
getLabel: () => find('remoteClusterFormServerNameFormRow').find('label').text(),
|
||||||
|
exists: () => exists(serverNameSelector),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalErrorExists = () => exists('remoteClusterFormGlobalError');
|
||||||
|
|
||||||
|
const createCloudUrlInputActions = () => {
|
||||||
|
const cloudUrlInputSelector = 'remoteClusterFormCloudUrlInput';
|
||||||
|
return {
|
||||||
|
cloudUrlInput: {
|
||||||
|
exists: () => exists(cloudUrlInputSelector),
|
||||||
|
getValue: () => find(cloudUrlInputSelector).props().value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
docsButtonExists,
|
||||||
|
...createPageTitleActions(),
|
||||||
|
...createNameInputActions(),
|
||||||
|
...createSkipUnavailableActions(),
|
||||||
|
...createConnectionModeActions(),
|
||||||
|
...createCloudUrlSwitchActions(),
|
||||||
|
...createSeedsInputActions(),
|
||||||
|
...createCloudUrlInputActions(),
|
||||||
|
...createProxyAddressActions(),
|
||||||
|
...createServerNameActions(),
|
||||||
|
...createSaveButtonActions(),
|
||||||
|
getErrorMessages: form.getErrorsMessages,
|
||||||
|
globalErrorExists,
|
||||||
|
};
|
||||||
|
};
|
|
@ -36,6 +36,8 @@ export const setupEnvironment = () => {
|
||||||
notificationServiceMock.createSetupContract().toasts,
|
notificationServiceMock.createSetupContract().toasts,
|
||||||
fatalErrorsServiceMock.createSetupContract()
|
fatalErrorsServiceMock.createSetupContract()
|
||||||
);
|
);
|
||||||
|
// This expects HttpSetup but we're giving it AxiosInstance.
|
||||||
|
// @ts-ignore
|
||||||
initHttp(mockHttpClient);
|
initHttp(mockHttpClient);
|
||||||
|
|
||||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
|
@ -5,10 +5,11 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
isCloudEnabled: boolean;
|
isCloudEnabled: boolean;
|
||||||
|
cloudBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext<Context>({} as any);
|
export const AppContext = createContext<Context>({} as any);
|
||||||
|
@ -22,3 +23,10 @@ export const AppContextProvider = ({
|
||||||
}) => {
|
}) => {
|
||||||
return <AppContext.Provider value={context}>{children}</AppContext.Provider>;
|
return <AppContext.Provider value={context}>{children}</AppContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAppContext = () => {
|
||||||
|
const ctx = useContext(AppContext);
|
||||||
|
if (!ctx) throw new Error('Cannot use outside of app context');
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
|
@ -12,7 +12,8 @@ export declare const renderApp: (
|
||||||
elem: HTMLElement | null,
|
elem: HTMLElement | null,
|
||||||
I18nContext: I18nStart['Context'],
|
I18nContext: I18nStart['Context'],
|
||||||
appDependencies: {
|
appDependencies: {
|
||||||
isCloudEnabled?: boolean;
|
isCloudEnabled: boolean;
|
||||||
|
cloudBaseUrl: string;
|
||||||
},
|
},
|
||||||
history: ScopedHistory
|
history: ScopedHistory
|
||||||
) => ReturnType<RegisterManagementAppArgs['mount']>;
|
) => ReturnType<RegisterManagementAppArgs['mount']>;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FunctionComponent, useState } from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { EuiLink, EuiPopover, EuiPopoverTitle, EuiText } from '@elastic/eui';
|
||||||
|
import { useAppContext } from '../../../../app_context';
|
||||||
|
|
||||||
|
export const CloudUrlHelp: FunctionComponent = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const { cloudBaseUrl } = useAppContext();
|
||||||
|
return (
|
||||||
|
<EuiPopover
|
||||||
|
button={
|
||||||
|
<EuiText size="xs">
|
||||||
|
<EuiLink
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel"
|
||||||
|
defaultMessage="Need help?"
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
</EuiText>
|
||||||
|
}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closePopover={() => setIsOpen(false)}
|
||||||
|
anchorPosition="upCenter"
|
||||||
|
>
|
||||||
|
<EuiPopoverTitle>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.popoverTitle"
|
||||||
|
defaultMessage="How to find your Elasticsearch endpoint URL"
|
||||||
|
/>
|
||||||
|
</EuiPopoverTitle>
|
||||||
|
<EuiText size="s" style={{ maxWidth: 500 }}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.stepOneText"
|
||||||
|
defaultMessage="Open the {deploymentsLink}, select the remote deployment and copy the {elasticsearch} endpoint URL."
|
||||||
|
values={{
|
||||||
|
deploymentsLink: (
|
||||||
|
<EuiLink external={true} href={`${cloudBaseUrl}/deployments`} target="_blank">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.cloudUrlHelpModal.deploymentsLink"
|
||||||
|
defaultMessage="deployments page"
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
elasticsearch: <strong> Elasticsearch</strong>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
</EuiPopover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
|
||||||
|
import { EuiDescribedFormGroup, EuiTitle, EuiFormRow, EuiSwitch, EuiSpacer } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants';
|
||||||
|
import { useAppContext } from '../../../../app_context';
|
||||||
|
|
||||||
|
import { ClusterErrors } from '../validators';
|
||||||
|
import { SniffConnection } from './sniff_connection';
|
||||||
|
import { ProxyConnection } from './proxy_connection';
|
||||||
|
import { FormFields } from '../remote_cluster_form';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
fields: FormFields;
|
||||||
|
onFieldsChange: (fields: Partial<FormFields>) => void;
|
||||||
|
fieldsErrors: ClusterErrors;
|
||||||
|
areErrorsVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectionMode: FunctionComponent<Props> = (props) => {
|
||||||
|
const { fields, onFieldsChange } = props;
|
||||||
|
const { mode, cloudUrlEnabled } = fields;
|
||||||
|
const { isCloudEnabled } = useAppContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiDescribedFormGroup
|
||||||
|
title={
|
||||||
|
<EuiTitle size="s">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionModeTitle"
|
||||||
|
defaultMessage="Connection mode"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{isCloudEnabled ? (
|
||||||
|
<>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription"
|
||||||
|
defaultMessage="Automatically configure the remote cluster by using the
|
||||||
|
Elasticsearch endpoint URL of the remote deployment or enter the proxy address and server name manually."
|
||||||
|
/>
|
||||||
|
<EuiFormRow hasEmptyLabelSpace fullWidth>
|
||||||
|
<EuiSwitch
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel"
|
||||||
|
defaultMessage="Manually enter proxy address and server name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checked={!cloudUrlEnabled}
|
||||||
|
data-test-subj="remoteClusterFormCloudUrlToggle"
|
||||||
|
onChange={(e) => onFieldsChange({ cloudUrlEnabled: !e.target.checked })}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionModeDescription"
|
||||||
|
defaultMessage="Use seed nodes by default, or switch to proxy mode."
|
||||||
|
/>
|
||||||
|
<EuiFormRow hasEmptyLabelSpace fullWidth>
|
||||||
|
<EuiSwitch
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldModeLabel"
|
||||||
|
defaultMessage="Use proxy mode"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
checked={mode === PROXY_MODE}
|
||||||
|
data-test-subj="remoteClusterFormConnectionModeToggle"
|
||||||
|
onChange={(e) =>
|
||||||
|
onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{mode === SNIFF_MODE ? <SniffConnection {...props} /> : <ProxyConnection {...props} />}
|
||||||
|
</EuiDescribedFormGroup>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { ConnectionMode } from './connection_mode';
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FunctionComponent } from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { EuiFieldNumber, EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
|
||||||
|
import { useAppContext } from '../../../../app_context';
|
||||||
|
import { proxySettingsUrl } from '../../../../services/documentation';
|
||||||
|
import { Props } from './connection_mode';
|
||||||
|
import { CloudUrlHelp } from './cloud_url_help';
|
||||||
|
|
||||||
|
export const ProxyConnection: FunctionComponent<Props> = (props) => {
|
||||||
|
const { fields, fieldsErrors, areErrorsVisible, onFieldsChange } = props;
|
||||||
|
const { isCloudEnabled } = useAppContext();
|
||||||
|
const { proxyAddress, serverName, proxySocketConnections, cloudUrl, cloudUrlEnabled } = fields;
|
||||||
|
const {
|
||||||
|
proxyAddress: proxyAddressError,
|
||||||
|
serverName: serverNameError,
|
||||||
|
cloudUrl: cloudUrlError,
|
||||||
|
} = fieldsErrors;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cloudUrlEnabled ? (
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormCloudUrlFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldCloudUrlLabel"
|
||||||
|
defaultMessage="Elasticsearch endpoint URL"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labelAppend={<CloudUrlHelp />}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && cloudUrlError)}
|
||||||
|
error={cloudUrlError}
|
||||||
|
fullWidth
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldCloudUrlHelpText"
|
||||||
|
defaultMessage="The protocol (https://) and port values are optional."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={cloudUrl}
|
||||||
|
onChange={(e) => onFieldsChange({ cloudUrl: e.target.value })}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && cloudUrlError)}
|
||||||
|
data-test-subj="remoteClusterFormCloudUrlInput"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormProxyAddressFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldProxyAddressLabel"
|
||||||
|
defaultMessage="Proxy address"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldProxyAddressHelpText"
|
||||||
|
defaultMessage="The address to use for remote connections."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && proxyAddressError)}
|
||||||
|
error={proxyAddressError}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={proxyAddress}
|
||||||
|
placeholder={i18n.translate(
|
||||||
|
'xpack.remoteClusters.remoteClusterForm.fieldProxyAddressPlaceholder',
|
||||||
|
{
|
||||||
|
defaultMessage: 'host:port',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onChange={(e) => onFieldsChange({ proxyAddress: e.target.value })}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && proxyAddressError)}
|
||||||
|
data-test-subj="remoteClusterFormProxyAddressInput"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormServerNameFormRow"
|
||||||
|
isInvalid={Boolean(areErrorsVisible && serverNameError)}
|
||||||
|
error={serverNameError}
|
||||||
|
label={
|
||||||
|
isCloudEnabled ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel"
|
||||||
|
defaultMessage="Server name"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameOptionalLabel"
|
||||||
|
defaultMessage="Server name (optional)"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameHelpText"
|
||||||
|
defaultMessage="A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. {learnMoreLink}"
|
||||||
|
values={{
|
||||||
|
learnMoreLink: (
|
||||||
|
<EuiLink href={proxySettingsUrl} target="_blank">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameHelpText.learnMoreLinkLabel"
|
||||||
|
defaultMessage="Learn more."
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={serverName}
|
||||||
|
onChange={(e) => onFieldsChange({ serverName: e.target.value })}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && serverNameError)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormProxySocketConnectionsFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldProxySocketConnectionsLabel"
|
||||||
|
defaultMessage="Socket connections"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText"
|
||||||
|
defaultMessage="The number of connections to open per remote cluster."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFieldNumber
|
||||||
|
value={proxySocketConnections || ''}
|
||||||
|
onChange={(e) => onFieldsChange({ proxySocketConnections: Number(e.target.value) })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FunctionComponent, useState } from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import {
|
||||||
|
EuiComboBox,
|
||||||
|
EuiComboBoxOptionOption,
|
||||||
|
EuiFieldNumber,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiLink,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { transportPortUrl } from '../../../../services/documentation';
|
||||||
|
import { validateSeed } from '../validators';
|
||||||
|
import { Props } from './connection_mode';
|
||||||
|
|
||||||
|
export const SniffConnection: FunctionComponent<Props> = ({
|
||||||
|
fields,
|
||||||
|
fieldsErrors,
|
||||||
|
areErrorsVisible,
|
||||||
|
onFieldsChange,
|
||||||
|
}) => {
|
||||||
|
const [localSeedErrors, setLocalSeedErrors] = useState<JSX.Element[]>([]);
|
||||||
|
const { seeds = [], nodeConnections } = fields;
|
||||||
|
const { seeds: seedsError } = fieldsErrors;
|
||||||
|
// Show errors if there is a general form error or local errors.
|
||||||
|
const areFormErrorsVisible = Boolean(areErrorsVisible && seedsError);
|
||||||
|
const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0;
|
||||||
|
const errors =
|
||||||
|
areFormErrorsVisible && seedsError ? localSeedErrors.concat(seedsError) : localSeedErrors;
|
||||||
|
const formattedSeeds: EuiComboBoxOptionOption[] = seeds.map((seed: string) => ({ label: seed }));
|
||||||
|
|
||||||
|
const onCreateSeed = (newSeed?: string) => {
|
||||||
|
// If the user just hit enter without typing anything, treat it as a no-op.
|
||||||
|
if (!newSeed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationErrors = validateSeed(newSeed);
|
||||||
|
|
||||||
|
if (validationErrors.length !== 0) {
|
||||||
|
setLocalSeedErrors(validationErrors);
|
||||||
|
// Return false to explicitly reject the user's input.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSeeds = seeds.slice(0);
|
||||||
|
newSeeds.push(newSeed.toLowerCase());
|
||||||
|
onFieldsChange({ seeds: newSeeds });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSeedsInputChange = (seedInput?: string) => {
|
||||||
|
if (!seedInput) {
|
||||||
|
// If empty seedInput ("") don't do anything. This happens
|
||||||
|
// right after a seed is created.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow typing to clear the errors, but not to add new ones.
|
||||||
|
const validationErrors =
|
||||||
|
!seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors;
|
||||||
|
|
||||||
|
// EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the
|
||||||
|
// input is a duplicate. So we need to surface this error here instead.
|
||||||
|
const isDuplicate = seeds.includes(seedInput);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
validationErrors.push(
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage"
|
||||||
|
defaultMessage="Duplicate seed nodes aren't allowed.`"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalSeedErrors(validationErrors);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormSeedNodesFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsLabel"
|
||||||
|
defaultMessage="Seed nodes"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsHelpText"
|
||||||
|
defaultMessage="An IP address or host name, followed by the {transportPort} of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
|
||||||
|
values={{
|
||||||
|
transportPort: (
|
||||||
|
<EuiLink href={transportPortUrl} target="_blank">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText"
|
||||||
|
defaultMessage="transport port"
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isInvalid={showErrors}
|
||||||
|
error={errors}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiComboBox
|
||||||
|
noSuggestions
|
||||||
|
placeholder={i18n.translate(
|
||||||
|
'xpack.remoteClusters.remoteClusterForm.fieldSeedsPlaceholder',
|
||||||
|
{
|
||||||
|
defaultMessage: 'host:port',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
selectedOptions={formattedSeeds}
|
||||||
|
onCreateOption={onCreateSeed}
|
||||||
|
onChange={(options: EuiComboBoxOptionOption[]) =>
|
||||||
|
onFieldsChange({ seeds: options.map(({ label }) => label) })
|
||||||
|
}
|
||||||
|
onSearchChange={onSeedsInputChange}
|
||||||
|
isInvalid={showErrors}
|
||||||
|
fullWidth
|
||||||
|
data-test-subj="remoteClusterFormSeedsInput"
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormNodeConnectionsFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldNodeConnectionsLabel"
|
||||||
|
defaultMessage="Node connections"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldNodeConnectionsHelpText"
|
||||||
|
defaultMessage="The number of gateway nodes to connect to for this cluster."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFieldNumber
|
||||||
|
value={nodeConnections || ''}
|
||||||
|
onChange={(e) => onFieldsChange({ nodeConnections: Number(e.target.value) })}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,962 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { merge } from 'lodash';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import {
|
|
||||||
EuiButton,
|
|
||||||
EuiButtonEmpty,
|
|
||||||
EuiCallOut,
|
|
||||||
EuiComboBox,
|
|
||||||
EuiDescribedFormGroup,
|
|
||||||
EuiFieldNumber,
|
|
||||||
EuiFieldText,
|
|
||||||
EuiFlexGroup,
|
|
||||||
EuiFlexItem,
|
|
||||||
EuiForm,
|
|
||||||
EuiFormRow,
|
|
||||||
EuiLink,
|
|
||||||
EuiLoadingKibana,
|
|
||||||
EuiLoadingSpinner,
|
|
||||||
EuiOverlayMask,
|
|
||||||
EuiSpacer,
|
|
||||||
EuiSwitch,
|
|
||||||
EuiText,
|
|
||||||
EuiTitle,
|
|
||||||
EuiDelayRender,
|
|
||||||
EuiScreenReaderOnly,
|
|
||||||
htmlIdGenerator,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
|
|
||||||
import {
|
|
||||||
skippingDisconnectedClustersUrl,
|
|
||||||
transportPortUrl,
|
|
||||||
proxySettingsUrl,
|
|
||||||
} from '../../../services/documentation';
|
|
||||||
|
|
||||||
import { RequestFlyout } from './request_flyout';
|
|
||||||
|
|
||||||
import {
|
|
||||||
validateName,
|
|
||||||
validateSeeds,
|
|
||||||
validateProxy,
|
|
||||||
validateSeed,
|
|
||||||
validateServerName,
|
|
||||||
} from './validators';
|
|
||||||
|
|
||||||
import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants';
|
|
||||||
|
|
||||||
import { AppContext } from '../../../app_context';
|
|
||||||
|
|
||||||
const defaultFields = {
|
|
||||||
name: '',
|
|
||||||
seeds: [],
|
|
||||||
skipUnavailable: false,
|
|
||||||
nodeConnections: 3,
|
|
||||||
proxyAddress: '',
|
|
||||||
proxySocketConnections: 18,
|
|
||||||
serverName: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ERROR_TITLE_ID = 'removeClustersErrorTitle';
|
|
||||||
const ERROR_LIST_ID = 'removeClustersErrorList';
|
|
||||||
|
|
||||||
export class RemoteClusterForm extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
save: PropTypes.func.isRequired,
|
|
||||||
cancel: PropTypes.func,
|
|
||||||
isSaving: PropTypes.bool,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
fields: PropTypes.object,
|
|
||||||
disabledFields: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
fields: merge({}, defaultFields),
|
|
||||||
disabledFields: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = AppContext;
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const { fields, disabledFields } = props;
|
|
||||||
const { isCloudEnabled } = context;
|
|
||||||
|
|
||||||
// Connection mode should default to "proxy" in cloud
|
|
||||||
const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE;
|
|
||||||
const fieldsState = merge({}, { ...defaultFields, mode: defaultMode }, fields);
|
|
||||||
|
|
||||||
this.generateId = htmlIdGenerator();
|
|
||||||
this.state = {
|
|
||||||
localSeedErrors: [],
|
|
||||||
seedInput: '',
|
|
||||||
fields: fieldsState,
|
|
||||||
disabledFields,
|
|
||||||
fieldsErrors: this.getFieldsErrors(fieldsState),
|
|
||||||
areErrorsVisible: false,
|
|
||||||
isRequestVisible: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRequest = () => {
|
|
||||||
this.setState(({ isRequestVisible }) => ({
|
|
||||||
isRequestVisible: !isRequestVisible,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
getFieldsErrors(fields, seedInput = '') {
|
|
||||||
const { name, seeds, mode, proxyAddress, serverName } = fields;
|
|
||||||
const { isCloudEnabled } = this.context;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: validateName(name),
|
|
||||||
seeds: mode === SNIFF_MODE ? validateSeeds(seeds, seedInput) : null,
|
|
||||||
proxyAddress: mode === PROXY_MODE ? validateProxy(proxyAddress) : null,
|
|
||||||
// server name is only required in cloud when proxy mode is enabled
|
|
||||||
serverName: isCloudEnabled && mode === PROXY_MODE ? validateServerName(serverName) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onFieldsChange = (changedFields) => {
|
|
||||||
this.setState(({ fields: prevFields, seedInput }) => {
|
|
||||||
const newFields = {
|
|
||||||
...prevFields,
|
|
||||||
...changedFields,
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
fields: newFields,
|
|
||||||
fieldsErrors: this.getFieldsErrors(newFields, seedInput),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getAllFields() {
|
|
||||||
const {
|
|
||||||
fields: {
|
|
||||||
name,
|
|
||||||
mode,
|
|
||||||
seeds,
|
|
||||||
nodeConnections,
|
|
||||||
proxyAddress,
|
|
||||||
proxySocketConnections,
|
|
||||||
serverName,
|
|
||||||
skipUnavailable,
|
|
||||||
},
|
|
||||||
} = this.state;
|
|
||||||
const { fields } = this.props;
|
|
||||||
|
|
||||||
let modeSettings;
|
|
||||||
|
|
||||||
if (mode === PROXY_MODE) {
|
|
||||||
modeSettings = {
|
|
||||||
proxyAddress,
|
|
||||||
proxySocketConnections,
|
|
||||||
serverName,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
modeSettings = {
|
|
||||||
seeds,
|
|
||||||
nodeConnections,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
skipUnavailable,
|
|
||||||
mode,
|
|
||||||
hasDeprecatedProxySetting: fields.hasDeprecatedProxySetting,
|
|
||||||
...modeSettings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
save = () => {
|
|
||||||
const { save } = this.props;
|
|
||||||
|
|
||||||
if (this.hasErrors()) {
|
|
||||||
this.setState({
|
|
||||||
areErrorsVisible: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cluster = this.getAllFields();
|
|
||||||
save(cluster);
|
|
||||||
};
|
|
||||||
|
|
||||||
onCreateSeed = (newSeed) => {
|
|
||||||
// If the user just hit enter without typing anything, treat it as a no-op.
|
|
||||||
if (!newSeed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localSeedErrors = validateSeed(newSeed);
|
|
||||||
|
|
||||||
if (localSeedErrors.length !== 0) {
|
|
||||||
this.setState({
|
|
||||||
localSeedErrors,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return false to explicitly reject the user's input.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
fields: { seeds },
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const newSeeds = seeds.slice(0);
|
|
||||||
newSeeds.push(newSeed.toLowerCase());
|
|
||||||
this.onFieldsChange({ seeds: newSeeds });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSeedsInputChange = (seedInput) => {
|
|
||||||
if (!seedInput) {
|
|
||||||
// If empty seedInput ("") don't do anything. This happens
|
|
||||||
// right after a seed is created.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(({ fields, localSeedErrors }) => {
|
|
||||||
const { seeds } = fields;
|
|
||||||
|
|
||||||
// Allow typing to clear the errors, but not to add new ones.
|
|
||||||
const errors = !seedInput || validateSeed(seedInput).length === 0 ? [] : localSeedErrors;
|
|
||||||
|
|
||||||
// EuiComboBox internally checks for duplicates and prevents calling onCreateOption if the
|
|
||||||
// input is a duplicate. So we need to surface this error here instead.
|
|
||||||
const isDuplicate = seeds.includes(seedInput);
|
|
||||||
|
|
||||||
if (isDuplicate) {
|
|
||||||
errors.push(
|
|
||||||
i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage', {
|
|
||||||
defaultMessage: `Duplicate seed nodes aren't allowed.`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
localSeedErrors: errors,
|
|
||||||
fieldsErrors: this.getFieldsErrors(fields, seedInput),
|
|
||||||
seedInput,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSeedsChange = (seeds) => {
|
|
||||||
this.onFieldsChange({ seeds: seeds.map(({ label }) => label) });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipUnavailableChange = (e) => {
|
|
||||||
const skipUnavailable = e.target.checked;
|
|
||||||
this.onFieldsChange({ skipUnavailable });
|
|
||||||
};
|
|
||||||
|
|
||||||
resetToDefault = (fieldName) => {
|
|
||||||
this.onFieldsChange({
|
|
||||||
[fieldName]: defaultFields[fieldName],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
hasErrors = () => {
|
|
||||||
const { fieldsErrors, localSeedErrors } = this.state;
|
|
||||||
const errorValues = Object.values(fieldsErrors);
|
|
||||||
const hasErrors = errorValues.some((error) => error != null) || localSeedErrors.length;
|
|
||||||
return hasErrors;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderSniffModeSettings() {
|
|
||||||
const {
|
|
||||||
areErrorsVisible,
|
|
||||||
fields: { seeds, nodeConnections },
|
|
||||||
fieldsErrors: { seeds: errorsSeeds },
|
|
||||||
localSeedErrors,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
// Show errors if there is a general form error or local errors.
|
|
||||||
const areFormErrorsVisible = Boolean(areErrorsVisible && errorsSeeds);
|
|
||||||
const showErrors = areFormErrorsVisible || localSeedErrors.length !== 0;
|
|
||||||
const errors = areFormErrorsVisible ? localSeedErrors.concat(errorsSeeds) : localSeedErrors;
|
|
||||||
|
|
||||||
const formattedSeeds = seeds.map((seed) => ({ label: seed }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormSeedNodesFormRow"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsLabel"
|
|
||||||
defaultMessage="Seed nodes"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldSeedsHelpText"
|
|
||||||
defaultMessage="An IP address or host name, followed by the {transportPort} of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
|
|
||||||
values={{
|
|
||||||
transportPort: (
|
|
||||||
<EuiLink href={transportPortUrl} target="_blank">
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText"
|
|
||||||
defaultMessage="transport port"
|
|
||||||
/>
|
|
||||||
</EuiLink>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isInvalid={showErrors}
|
|
||||||
error={errors}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiComboBox
|
|
||||||
noSuggestions
|
|
||||||
placeholder={i18n.translate(
|
|
||||||
'xpack.remoteClusters.remoteClusterForm.fieldSeedsPlaceholder',
|
|
||||||
{
|
|
||||||
defaultMessage: 'host:port',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
selectedOptions={formattedSeeds}
|
|
||||||
onCreateOption={this.onCreateSeed}
|
|
||||||
onChange={this.onSeedsChange}
|
|
||||||
onSearchChange={this.onSeedsInputChange}
|
|
||||||
isInvalid={showErrors}
|
|
||||||
fullWidth
|
|
||||||
data-test-subj="remoteClusterFormSeedsInput"
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormNodeConnectionsFormRow"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldNodeConnectionsLabel"
|
|
||||||
defaultMessage="Node connections"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldNodeConnectionsHelpText"
|
|
||||||
defaultMessage="The number of gateway nodes to connect to for this cluster."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFieldNumber
|
|
||||||
value={nodeConnections || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
this.onFieldsChange({ nodeConnections: Number(e.target.value) || null })
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderProxyModeSettings() {
|
|
||||||
const {
|
|
||||||
areErrorsVisible,
|
|
||||||
fields: { proxyAddress, proxySocketConnections, serverName },
|
|
||||||
fieldsErrors: { proxyAddress: errorProxyAddress, serverName: errorServerName },
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const { isCloudEnabled } = this.context;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormProxyAddressFormRow"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldProxyAddressLabel"
|
|
||||||
defaultMessage="Proxy address"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldProxyAddressHelpText"
|
|
||||||
defaultMessage="The address to use for remote connections."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorProxyAddress)}
|
|
||||||
error={errorProxyAddress}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFieldText
|
|
||||||
value={proxyAddress}
|
|
||||||
placeholder={i18n.translate(
|
|
||||||
'xpack.remoteClusters.remoteClusterForm.fieldProxyAddressPlaceholder',
|
|
||||||
{
|
|
||||||
defaultMessage: 'host:port',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onChange={(e) => this.onFieldsChange({ proxyAddress: e.target.value })}
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorProxyAddress)}
|
|
||||||
data-test-subj="remoteClusterFormProxyAddressInput"
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormServerNameFormRow"
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorServerName)}
|
|
||||||
error={errorServerName}
|
|
||||||
label={
|
|
||||||
isCloudEnabled ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel"
|
|
||||||
defaultMessage="Server name"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameOptionalLabel"
|
|
||||||
defaultMessage="Server name (optional)"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameHelpText"
|
|
||||||
defaultMessage="A string sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. {learnMoreLink}"
|
|
||||||
values={{
|
|
||||||
learnMoreLink: (
|
|
||||||
<EuiLink href={proxySettingsUrl} target="_blank">
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldServerNameHelpText.learnMoreLinkLabel"
|
|
||||||
defaultMessage="Learn more."
|
|
||||||
/>
|
|
||||||
</EuiLink>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFieldText
|
|
||||||
value={serverName}
|
|
||||||
onChange={(e) => this.onFieldsChange({ serverName: e.target.value })}
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorServerName)}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormProxySocketConnectionsFormRow"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldProxySocketConnectionsLabel"
|
|
||||||
defaultMessage="Socket connections"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText"
|
|
||||||
defaultMessage="The number of socket connections to open per remote cluster."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFieldNumber
|
|
||||||
value={proxySocketConnections || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null })
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMode() {
|
|
||||||
const {
|
|
||||||
fields: { mode },
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const { isCloudEnabled } = this.context;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiDescribedFormGroup
|
|
||||||
title={
|
|
||||||
<EuiTitle size="s">
|
|
||||||
<h2>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionModeTitle"
|
|
||||||
defaultMessage="Connection mode"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
</EuiTitle>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionModeDescription"
|
|
||||||
defaultMessage="Use seed nodes by default, or switch to proxy mode."
|
|
||||||
/>
|
|
||||||
<EuiFormRow hasEmptyLabelSpace fullWidth>
|
|
||||||
<EuiSwitch
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldModeLabel"
|
|
||||||
defaultMessage="Use proxy mode"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
checked={mode === PROXY_MODE}
|
|
||||||
data-test-subj="remoteClusterFormConnectionModeToggle"
|
|
||||||
onChange={(e) =>
|
|
||||||
this.onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
{isCloudEnabled && mode === PROXY_MODE ? (
|
|
||||||
<>
|
|
||||||
<EuiSpacer size="s" />
|
|
||||||
<EuiCallOut
|
|
||||||
iconType="pin"
|
|
||||||
size="s"
|
|
||||||
title={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.cloudClusterInformationTitle"
|
|
||||||
defaultMessage="Use proxy mode for Elastic Cloud deployment"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.cloudClusterInformationDescription"
|
|
||||||
defaultMessage="To find the proxy address and server name of your cluster, go to the {security} page of your deployment menu and search for {searchString}."
|
|
||||||
values={{
|
|
||||||
security: (
|
|
||||||
<strong>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.cloudClusterSecurityDescription"
|
|
||||||
defaultMessage="Security"
|
|
||||||
/>
|
|
||||||
</strong>
|
|
||||||
),
|
|
||||||
searchString: (
|
|
||||||
<strong>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.cloudClusterSearchDescription"
|
|
||||||
defaultMessage="Remote cluster parameters"
|
|
||||||
/>
|
|
||||||
</strong>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</EuiCallOut>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{mode === PROXY_MODE ? this.renderProxyModeSettings() : this.renderSniffModeSettings()}
|
|
||||||
</EuiDescribedFormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSkipUnavailable() {
|
|
||||||
const {
|
|
||||||
fields: { skipUnavailable },
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiDescribedFormGroup
|
|
||||||
title={
|
|
||||||
<EuiTitle size="s">
|
|
||||||
<h2>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle"
|
|
||||||
defaultMessage="Make remote cluster optional"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
</EuiTitle>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription"
|
|
||||||
defaultMessage="A request fails if any of the queried remote clusters are unavailable. To send requests to other remote clusters if this cluster is unavailable, enable {optionName}. {learnMoreLink}"
|
|
||||||
values={{
|
|
||||||
optionName: (
|
|
||||||
<strong>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel"
|
|
||||||
defaultMessage="Skip if unavailable"
|
|
||||||
/>
|
|
||||||
</strong>
|
|
||||||
),
|
|
||||||
learnMoreLink: (
|
|
||||||
<EuiLink href={skippingDisconnectedClustersUrl} target="_blank">
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel"
|
|
||||||
defaultMessage="Learn more."
|
|
||||||
/>
|
|
||||||
</EuiLink>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormSkipUnavailableFormRow"
|
|
||||||
className="remoteClusterSkipIfUnavailableSwitch"
|
|
||||||
hasEmptyLabelSpace
|
|
||||||
fullWidth
|
|
||||||
helpText={
|
|
||||||
skipUnavailable !== defaultFields.skipUnavailable ? (
|
|
||||||
<EuiLink
|
|
||||||
onClick={() => {
|
|
||||||
this.resetToDefault('skipUnavailable');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel"
|
|
||||||
defaultMessage="Reset to default"
|
|
||||||
/>
|
|
||||||
</EuiLink>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<EuiSwitch
|
|
||||||
label={i18n.translate(
|
|
||||||
'xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Skip if unavailable',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
checked={skipUnavailable}
|
|
||||||
onChange={this.onSkipUnavailableChange}
|
|
||||||
data-test-subj="remoteClusterFormSkipUnavailableFormToggle"
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiDescribedFormGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderActions() {
|
|
||||||
const { isSaving, cancel } = this.props;
|
|
||||||
const { areErrorsVisible, isRequestVisible } = this.state;
|
|
||||||
|
|
||||||
if (isSaving) {
|
|
||||||
return (
|
|
||||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiLoadingSpinner size="l" />
|
|
||||||
</EuiFlexItem>
|
|
||||||
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiText>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.actions.savingText"
|
|
||||||
defaultMessage="Saving"
|
|
||||||
/>
|
|
||||||
</EuiText>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelButton;
|
|
||||||
|
|
||||||
if (cancel) {
|
|
||||||
cancelButton = (
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButtonEmpty color="primary" onClick={cancel}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
|
|
||||||
defaultMessage="Cancel"
|
|
||||||
/>
|
|
||||||
</EuiButtonEmpty>
|
|
||||||
</EuiFlexItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSaveDisabled = areErrorsVisible && this.hasErrors();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButton
|
|
||||||
data-test-subj="remoteClusterFormSaveButton"
|
|
||||||
color="secondary"
|
|
||||||
iconType="check"
|
|
||||||
onClick={this.save}
|
|
||||||
fill
|
|
||||||
disabled={isSaveDisabled}
|
|
||||||
aria-describedby={`${this.generateId(ERROR_TITLE_ID)} ${this.generateId(
|
|
||||||
ERROR_LIST_ID
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.saveButtonLabel"
|
|
||||||
defaultMessage="Save"
|
|
||||||
/>
|
|
||||||
</EuiButton>
|
|
||||||
</EuiFlexItem>
|
|
||||||
|
|
||||||
{cancelButton}
|
|
||||||
</EuiFlexGroup>
|
|
||||||
</EuiFlexItem>
|
|
||||||
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiButtonEmpty onClick={this.toggleRequest}>
|
|
||||||
{isRequestVisible ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel"
|
|
||||||
defaultMessage="Hide request"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.showRequestButtonLabel"
|
|
||||||
defaultMessage="Show request"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EuiButtonEmpty>
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSavingFeedback() {
|
|
||||||
if (this.props.isSaving) {
|
|
||||||
return (
|
|
||||||
<EuiOverlayMask>
|
|
||||||
<EuiLoadingKibana size="xl" />
|
|
||||||
</EuiOverlayMask>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSaveErrorFeedback() {
|
|
||||||
const { saveError } = this.props;
|
|
||||||
|
|
||||||
if (saveError) {
|
|
||||||
const { message, cause } = saveError;
|
|
||||||
|
|
||||||
let errorBody;
|
|
||||||
|
|
||||||
if (cause && Array.isArray(cause)) {
|
|
||||||
if (cause.length === 1) {
|
|
||||||
errorBody = <p>{cause[0]}</p>;
|
|
||||||
} else {
|
|
||||||
errorBody = (
|
|
||||||
<ul>
|
|
||||||
{cause.map((causeValue) => (
|
|
||||||
<li key={causeValue}>{causeValue}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EuiCallOut title={message} icon="cross" color="warning">
|
|
||||||
{errorBody}
|
|
||||||
</EuiCallOut>
|
|
||||||
|
|
||||||
<EuiSpacer />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderErrors = () => {
|
|
||||||
const {
|
|
||||||
areErrorsVisible,
|
|
||||||
fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress },
|
|
||||||
localSeedErrors,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const hasErrors = this.hasErrors();
|
|
||||||
|
|
||||||
if (!areErrorsVisible || !hasErrors) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorExplanations = [];
|
|
||||||
|
|
||||||
if (errorClusterName) {
|
|
||||||
errorExplanations.push({
|
|
||||||
key: 'nameExplanation',
|
|
||||||
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', {
|
|
||||||
defaultMessage: 'The "Name" field is invalid.',
|
|
||||||
}),
|
|
||||||
error: errorClusterName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorsSeeds) {
|
|
||||||
errorExplanations.push({
|
|
||||||
key: 'seedsExplanation',
|
|
||||||
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', {
|
|
||||||
defaultMessage: 'The "Seed nodes" field is invalid.',
|
|
||||||
}),
|
|
||||||
error: errorsSeeds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localSeedErrors && localSeedErrors.length) {
|
|
||||||
errorExplanations.push({
|
|
||||||
key: 'localSeedExplanation',
|
|
||||||
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage', {
|
|
||||||
defaultMessage: 'The "Seed nodes" field is invalid.',
|
|
||||||
}),
|
|
||||||
error: localSeedErrors.join(' '),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorProxyAddress) {
|
|
||||||
errorExplanations.push({
|
|
||||||
key: 'seedsExplanation',
|
|
||||||
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', {
|
|
||||||
defaultMessage: 'The "Proxy address" field is invalid.',
|
|
||||||
}),
|
|
||||||
error: errorProxyAddress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const messagesToBeRendered = errorExplanations.length && (
|
|
||||||
<EuiScreenReaderOnly>
|
|
||||||
<dl id={this.generateId(ERROR_LIST_ID)} aria-labelledby={this.generateId(ERROR_TITLE_ID)}>
|
|
||||||
{errorExplanations.map(({ key, field, error }) => (
|
|
||||||
<div key={key}>
|
|
||||||
<dt>{field}</dt>
|
|
||||||
<dd>{error}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</EuiScreenReaderOnly>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
|
|
||||||
<EuiCallOut
|
|
||||||
title={
|
|
||||||
<h3 id={this.generateId(ERROR_TITLE_ID)}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
|
|
||||||
defaultMessage="Fix errors before continuing."
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
}
|
|
||||||
color="danger"
|
|
||||||
iconType="cross"
|
|
||||||
/>
|
|
||||||
<EuiDelayRender>{messagesToBeRendered}</EuiDelayRender>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
disabledFields: { name: disabledName },
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isRequestVisible,
|
|
||||||
areErrorsVisible,
|
|
||||||
fields: { name },
|
|
||||||
fieldsErrors: { name: errorClusterName },
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{this.renderSaveErrorFeedback()}
|
|
||||||
|
|
||||||
<EuiForm data-test-subj="remoteClusterForm">
|
|
||||||
<EuiDescribedFormGroup
|
|
||||||
title={
|
|
||||||
<EuiTitle size="s">
|
|
||||||
<h2>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
|
|
||||||
defaultMessage="Name"
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
</EuiTitle>
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.sectionNameDescription"
|
|
||||||
defaultMessage="A unique name for the cluster."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFormRow
|
|
||||||
data-test-subj="remoteClusterFormNameFormRow"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabel"
|
|
||||||
defaultMessage="Name"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
helpText={
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabelHelpText"
|
|
||||||
defaultMessage="Name can only contain letters, numbers, underscores, and dashes."
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
error={errorClusterName}
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<EuiFieldText
|
|
||||||
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => this.onFieldsChange({ name: e.target.value })}
|
|
||||||
fullWidth
|
|
||||||
disabled={disabledName}
|
|
||||||
data-test-subj="remoteClusterFormNameInput"
|
|
||||||
/>
|
|
||||||
</EuiFormRow>
|
|
||||||
</EuiDescribedFormGroup>
|
|
||||||
|
|
||||||
{this.renderMode()}
|
|
||||||
|
|
||||||
{this.renderSkipUnavailable()}
|
|
||||||
</EuiForm>
|
|
||||||
|
|
||||||
{this.renderErrors()}
|
|
||||||
|
|
||||||
<EuiSpacer size="l" />
|
|
||||||
|
|
||||||
{this.renderActions()}
|
|
||||||
|
|
||||||
{this.renderSavingFeedback()}
|
|
||||||
|
|
||||||
{isRequestVisible ? (
|
|
||||||
<RequestFlyout
|
|
||||||
name={name}
|
|
||||||
cluster={this.getAllFields()}
|
|
||||||
close={() => this.setState({ isRequestVisible: false })}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { mountWithIntl, renderWithIntl } from '@kbn/test/jest';
|
|
||||||
import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test';
|
|
||||||
import { RemoteClusterForm } from './remote_cluster_form';
|
|
||||||
|
|
||||||
// Make sure we have deterministic aria IDs.
|
|
||||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
|
||||||
htmlIdGenerator: (prefix = 'staticGenerator') => (suffix = 'staticId') => `${prefix}_${suffix}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('RemoteClusterForm', () => {
|
|
||||||
test(`renders untouched state`, () => {
|
|
||||||
const component = renderWithIntl(<RemoteClusterForm save={() => {}} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('proxy mode', () => {
|
|
||||||
test('renders correct connection settings when user enables proxy mode', () => {
|
|
||||||
const component = mountWithIntl(<RemoteClusterForm save={() => {}} />);
|
|
||||||
|
|
||||||
findTestSubject(component, 'remoteClusterFormConnectionModeToggle').simulate('click');
|
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('validation', () => {
|
|
||||||
test('renders invalid state and a global form error when the user tries to submit an invalid form', () => {
|
|
||||||
const component = mountWithIntl(<RemoteClusterForm save={() => {}} />);
|
|
||||||
|
|
||||||
findTestSubject(component, 'remoteClusterFormSaveButton').simulate('click');
|
|
||||||
|
|
||||||
const fieldsSnapshot = [
|
|
||||||
'remoteClusterFormNameFormRow',
|
|
||||||
'remoteClusterFormSeedNodesFormRow',
|
|
||||||
'remoteClusterFormSkipUnavailableFormRow',
|
|
||||||
'remoteClusterFormGlobalError',
|
|
||||||
].map((testSubject) => {
|
|
||||||
const mountedField = findTestSubject(component, testSubject);
|
|
||||||
return takeMountedSnapshot(mountedField);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(fieldsSnapshot).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,629 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { merge } from 'lodash';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiCallOut,
|
||||||
|
EuiDescribedFormGroup,
|
||||||
|
EuiFieldText,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiForm,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiLink,
|
||||||
|
EuiLoadingKibana,
|
||||||
|
EuiLoadingSpinner,
|
||||||
|
EuiOverlayMask,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiSwitch,
|
||||||
|
EuiText,
|
||||||
|
EuiTitle,
|
||||||
|
EuiDelayRender,
|
||||||
|
EuiScreenReaderOnly,
|
||||||
|
htmlIdGenerator,
|
||||||
|
EuiSwitchEvent,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import { Cluster } from '../../../../../common/lib';
|
||||||
|
import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants';
|
||||||
|
|
||||||
|
import { AppContext, Context } from '../../../app_context';
|
||||||
|
|
||||||
|
import { skippingDisconnectedClustersUrl } from '../../../services/documentation';
|
||||||
|
|
||||||
|
import { RequestFlyout } from './request_flyout';
|
||||||
|
import { ConnectionMode } from './components';
|
||||||
|
import {
|
||||||
|
ClusterErrors,
|
||||||
|
convertCloudUrlToProxyConnection,
|
||||||
|
convertProxyConnectionToCloudUrl,
|
||||||
|
validateCluster,
|
||||||
|
} from './validators';
|
||||||
|
import { isCloudUrlEnabled } from './validators/validate_cloud_url';
|
||||||
|
|
||||||
|
const defaultClusterValues: Cluster = {
|
||||||
|
name: '',
|
||||||
|
seeds: [],
|
||||||
|
skipUnavailable: false,
|
||||||
|
nodeConnections: 3,
|
||||||
|
proxyAddress: '',
|
||||||
|
proxySocketConnections: 18,
|
||||||
|
serverName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ERROR_TITLE_ID = 'removeClustersErrorTitle';
|
||||||
|
const ERROR_LIST_ID = 'removeClustersErrorList';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
save: (cluster: Cluster) => void;
|
||||||
|
cancel?: () => void;
|
||||||
|
isSaving?: boolean;
|
||||||
|
saveError?: any;
|
||||||
|
cluster?: Cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormFields = Cluster & { cloudUrl: string; cloudUrlEnabled: boolean };
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
fields: FormFields;
|
||||||
|
fieldsErrors: ClusterErrors;
|
||||||
|
areErrorsVisible: boolean;
|
||||||
|
isRequestVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteClusterForm extends Component<Props, State> {
|
||||||
|
static defaultProps = {
|
||||||
|
fields: merge({}, defaultClusterValues),
|
||||||
|
};
|
||||||
|
|
||||||
|
static contextType = AppContext;
|
||||||
|
private readonly generateId: (idSuffix?: string) => string;
|
||||||
|
|
||||||
|
constructor(props: Props, context: Context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const { cluster } = props;
|
||||||
|
const { isCloudEnabled } = context;
|
||||||
|
|
||||||
|
// Connection mode should default to "proxy" in cloud
|
||||||
|
const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE;
|
||||||
|
const fieldsState: FormFields = merge(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...defaultClusterValues,
|
||||||
|
mode: defaultMode,
|
||||||
|
cloudUrl: convertProxyConnectionToCloudUrl(cluster),
|
||||||
|
cloudUrlEnabled: isCloudEnabled && isCloudUrlEnabled(cluster),
|
||||||
|
},
|
||||||
|
cluster
|
||||||
|
);
|
||||||
|
|
||||||
|
this.generateId = htmlIdGenerator();
|
||||||
|
this.state = {
|
||||||
|
fields: fieldsState,
|
||||||
|
fieldsErrors: validateCluster(fieldsState, isCloudEnabled),
|
||||||
|
areErrorsVisible: false,
|
||||||
|
isRequestVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRequest = () => {
|
||||||
|
this.setState(({ isRequestVisible }) => ({
|
||||||
|
isRequestVisible: !isRequestVisible,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
onFieldsChange = (changedFields: Partial<FormFields>) => {
|
||||||
|
const { isCloudEnabled } = this.context;
|
||||||
|
|
||||||
|
// when cloudUrl changes, fill proxy address and server name
|
||||||
|
const { cloudUrl } = changedFields;
|
||||||
|
if (cloudUrl) {
|
||||||
|
const { proxyAddress, serverName } = convertCloudUrlToProxyConnection(cloudUrl);
|
||||||
|
changedFields = {
|
||||||
|
...changedFields,
|
||||||
|
proxyAddress,
|
||||||
|
serverName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(({ fields: prevFields }) => {
|
||||||
|
const newFields = {
|
||||||
|
...prevFields,
|
||||||
|
...changedFields,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
fields: newFields,
|
||||||
|
fieldsErrors: validateCluster(newFields, isCloudEnabled),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getCluster(): Cluster {
|
||||||
|
const {
|
||||||
|
fields: {
|
||||||
|
name,
|
||||||
|
mode,
|
||||||
|
seeds,
|
||||||
|
nodeConnections,
|
||||||
|
proxyAddress,
|
||||||
|
proxySocketConnections,
|
||||||
|
serverName,
|
||||||
|
skipUnavailable,
|
||||||
|
},
|
||||||
|
} = this.state;
|
||||||
|
const { cluster } = this.props;
|
||||||
|
|
||||||
|
let modeSettings;
|
||||||
|
|
||||||
|
if (mode === PROXY_MODE) {
|
||||||
|
modeSettings = {
|
||||||
|
proxyAddress,
|
||||||
|
proxySocketConnections,
|
||||||
|
serverName,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
modeSettings = {
|
||||||
|
seeds,
|
||||||
|
nodeConnections,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
skipUnavailable,
|
||||||
|
mode,
|
||||||
|
hasDeprecatedProxySetting: cluster?.hasDeprecatedProxySetting,
|
||||||
|
...modeSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
save = () => {
|
||||||
|
const { save } = this.props;
|
||||||
|
|
||||||
|
if (this.hasErrors()) {
|
||||||
|
this.setState({
|
||||||
|
areErrorsVisible: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cluster = this.getCluster();
|
||||||
|
save(cluster);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSkipUnavailableChange = (e: EuiSwitchEvent) => {
|
||||||
|
const skipUnavailable = e.target.checked;
|
||||||
|
this.onFieldsChange({ skipUnavailable });
|
||||||
|
};
|
||||||
|
|
||||||
|
resetToDefault = (fieldName: keyof Cluster) => {
|
||||||
|
this.onFieldsChange({
|
||||||
|
[fieldName]: defaultClusterValues[fieldName],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hasErrors = () => {
|
||||||
|
const { fieldsErrors } = this.state;
|
||||||
|
const errorValues = Object.values(fieldsErrors);
|
||||||
|
return errorValues.some((error) => error != null);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderSkipUnavailable() {
|
||||||
|
const {
|
||||||
|
fields: { skipUnavailable },
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiDescribedFormGroup
|
||||||
|
title={
|
||||||
|
<EuiTitle size="s">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle"
|
||||||
|
defaultMessage="Make remote cluster optional"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<Fragment>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription"
|
||||||
|
defaultMessage="If any of the remote clusters are unavailable, the query request fails. To avoid this and continue to send requests to other clusters, enable {optionName}. {learnMoreLink}"
|
||||||
|
values={{
|
||||||
|
optionName: (
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel"
|
||||||
|
defaultMessage="Skip if unavailable"
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
learnMoreLink: (
|
||||||
|
<EuiLink href={skippingDisconnectedClustersUrl} target="_blank">
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel"
|
||||||
|
defaultMessage="Learn more."
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormSkipUnavailableFormRow"
|
||||||
|
className="remoteClusterSkipIfUnavailableSwitch"
|
||||||
|
hasEmptyLabelSpace
|
||||||
|
fullWidth
|
||||||
|
helpText={
|
||||||
|
skipUnavailable !== defaultClusterValues.skipUnavailable ? (
|
||||||
|
<EuiLink
|
||||||
|
onClick={() => {
|
||||||
|
this.resetToDefault('skipUnavailable');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel"
|
||||||
|
defaultMessage="Reset to default"
|
||||||
|
/>
|
||||||
|
</EuiLink>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiSwitch
|
||||||
|
label={i18n.translate(
|
||||||
|
'xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Skip if unavailable',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
checked={!!skipUnavailable}
|
||||||
|
onChange={this.onSkipUnavailableChange}
|
||||||
|
data-test-subj="remoteClusterFormSkipUnavailableFormToggle"
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiDescribedFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions() {
|
||||||
|
const { isSaving, cancel } = this.props;
|
||||||
|
const { areErrorsVisible, isRequestVisible } = this.state;
|
||||||
|
|
||||||
|
if (isSaving) {
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiLoadingSpinner size="l" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiText>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.actions.savingText"
|
||||||
|
defaultMessage="Saving"
|
||||||
|
/>
|
||||||
|
</EuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelButton;
|
||||||
|
|
||||||
|
if (cancel) {
|
||||||
|
cancelButton = (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty color="primary" onClick={cancel}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
|
||||||
|
defaultMessage="Cancel"
|
||||||
|
/>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSaveDisabled = areErrorsVisible && this.hasErrors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
data-test-subj="remoteClusterFormSaveButton"
|
||||||
|
color="secondary"
|
||||||
|
iconType="check"
|
||||||
|
onClick={this.save}
|
||||||
|
fill
|
||||||
|
isDisabled={isSaveDisabled}
|
||||||
|
aria-describedby={`${this.generateId(ERROR_TITLE_ID)} ${this.generateId(
|
||||||
|
ERROR_LIST_ID
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.saveButtonLabel"
|
||||||
|
defaultMessage="Save"
|
||||||
|
/>
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
{cancelButton}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty onClick={this.toggleRequest}>
|
||||||
|
{isRequestVisible ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel"
|
||||||
|
defaultMessage="Hide request"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.showRequestButtonLabel"
|
||||||
|
defaultMessage="Show request"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSavingFeedback() {
|
||||||
|
if (this.props.isSaving) {
|
||||||
|
return (
|
||||||
|
<EuiOverlayMask>
|
||||||
|
<EuiLoadingKibana size="xl" />
|
||||||
|
</EuiOverlayMask>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSaveErrorFeedback() {
|
||||||
|
const { saveError } = this.props;
|
||||||
|
|
||||||
|
if (saveError) {
|
||||||
|
const { message, cause } = saveError;
|
||||||
|
|
||||||
|
let errorBody;
|
||||||
|
|
||||||
|
if (cause && Array.isArray(cause)) {
|
||||||
|
if (cause.length === 1) {
|
||||||
|
errorBody = <p>{cause[0]}</p>;
|
||||||
|
} else {
|
||||||
|
errorBody = (
|
||||||
|
<ul>
|
||||||
|
{cause.map((causeValue) => (
|
||||||
|
<li key={causeValue}>{causeValue}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiCallOut title={message} iconType="cross" color="warning">
|
||||||
|
{errorBody}
|
||||||
|
</EuiCallOut>
|
||||||
|
|
||||||
|
<EuiSpacer />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderErrors = () => {
|
||||||
|
const {
|
||||||
|
areErrorsVisible,
|
||||||
|
fieldsErrors: {
|
||||||
|
name: errorClusterName,
|
||||||
|
seeds: errorsSeeds,
|
||||||
|
proxyAddress: errorProxyAddress,
|
||||||
|
serverName: errorServerName,
|
||||||
|
cloudUrl: errorCloudUrl,
|
||||||
|
},
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const hasErrors = this.hasErrors();
|
||||||
|
|
||||||
|
if (!areErrorsVisible || !hasErrors) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorExplanations = [];
|
||||||
|
|
||||||
|
if (errorClusterName) {
|
||||||
|
errorExplanations.push({
|
||||||
|
key: 'nameExplanation',
|
||||||
|
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', {
|
||||||
|
defaultMessage: 'The "Name" field is invalid.',
|
||||||
|
}),
|
||||||
|
error: errorClusterName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorsSeeds) {
|
||||||
|
errorExplanations.push({
|
||||||
|
key: 'seedsExplanation',
|
||||||
|
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', {
|
||||||
|
defaultMessage: 'The "Seed nodes" field is invalid.',
|
||||||
|
}),
|
||||||
|
error: errorsSeeds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorProxyAddress) {
|
||||||
|
errorExplanations.push({
|
||||||
|
key: 'proxyAddressExplanation',
|
||||||
|
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', {
|
||||||
|
defaultMessage: 'The "Proxy address" field is invalid.',
|
||||||
|
}),
|
||||||
|
error: errorProxyAddress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorServerName) {
|
||||||
|
errorExplanations.push({
|
||||||
|
key: 'serverNameExplanation',
|
||||||
|
field: i18n.translate(
|
||||||
|
'xpack.remoteClusters.remoteClusterForm.inputServerNameErrorMessage',
|
||||||
|
{
|
||||||
|
defaultMessage: 'The "Server name" field is invalid.',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
error: errorServerName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCloudUrl) {
|
||||||
|
errorExplanations.push({
|
||||||
|
key: 'cloudUrlExplanation',
|
||||||
|
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputcloudUrlErrorMessage', {
|
||||||
|
defaultMessage: 'The "Elasticsearch endpoint URL" field is invalid.',
|
||||||
|
}),
|
||||||
|
error: errorCloudUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesToBeRendered = errorExplanations.length && (
|
||||||
|
<EuiScreenReaderOnly>
|
||||||
|
<dl id={this.generateId(ERROR_LIST_ID)} aria-labelledby={this.generateId(ERROR_TITLE_ID)}>
|
||||||
|
{errorExplanations.map(({ key, field, error }) => (
|
||||||
|
<div key={key}>
|
||||||
|
<dt>{field}</dt>
|
||||||
|
<dd>{error}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</EuiScreenReaderOnly>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
|
||||||
|
<EuiCallOut
|
||||||
|
title={
|
||||||
|
<h3 id={this.generateId(ERROR_TITLE_ID)}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
|
||||||
|
defaultMessage="Fix errors before continuing."
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
}
|
||||||
|
color="danger"
|
||||||
|
iconType="cross"
|
||||||
|
/>
|
||||||
|
<EuiDelayRender>{messagesToBeRendered}</EuiDelayRender>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isRequestVisible, areErrorsVisible, fields, fieldsErrors } = this.state;
|
||||||
|
const { name: errorClusterName } = fieldsErrors;
|
||||||
|
const { cluster } = this.props;
|
||||||
|
const isNew = !cluster;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{this.renderSaveErrorFeedback()}
|
||||||
|
|
||||||
|
<EuiForm data-test-subj="remoteClusterForm">
|
||||||
|
<EuiDescribedFormGroup
|
||||||
|
title={
|
||||||
|
<EuiTitle size="s">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
|
||||||
|
defaultMessage="Name"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.sectionNameDescription"
|
||||||
|
defaultMessage="A unique name for the cluster."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFormRow
|
||||||
|
data-test-subj="remoteClusterFormNameFormRow"
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabel"
|
||||||
|
defaultMessage="Name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabelHelpText"
|
||||||
|
defaultMessage="Must contain only letters, numbers, underscores, and dashes."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
error={errorClusterName}
|
||||||
|
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
isInvalid={Boolean(areErrorsVisible && errorClusterName)}
|
||||||
|
value={fields.name}
|
||||||
|
onChange={(e) => this.onFieldsChange({ name: e.target.value })}
|
||||||
|
fullWidth
|
||||||
|
disabled={!isNew}
|
||||||
|
data-test-subj="remoteClusterFormNameInput"
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</EuiDescribedFormGroup>
|
||||||
|
|
||||||
|
<ConnectionMode
|
||||||
|
fields={fields}
|
||||||
|
fieldsErrors={fieldsErrors}
|
||||||
|
onFieldsChange={this.onFieldsChange}
|
||||||
|
areErrorsVisible={areErrorsVisible}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{this.renderSkipUnavailable()}
|
||||||
|
</EuiForm>
|
||||||
|
|
||||||
|
{this.renderErrors()}
|
||||||
|
|
||||||
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
|
{this.renderActions()}
|
||||||
|
|
||||||
|
{this.renderSavingFeedback()}
|
||||||
|
|
||||||
|
{isRequestVisible ? (
|
||||||
|
<RequestFlyout
|
||||||
|
cluster={this.getCluster()}
|
||||||
|
close={() => this.setState({ isRequestVisible: false })}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,13 +24,13 @@ import { Cluster, serializeCluster } from '../../../../../common/lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
name: string;
|
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequestFlyout extends PureComponent<Props> {
|
export class RequestFlyout extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { name, close, cluster } = this.props;
|
const { close, cluster } = this.props;
|
||||||
|
const { name } = cluster;
|
||||||
const endpoint = 'PUT _cluster/settings';
|
const endpoint = 'PUT _cluster/settings';
|
||||||
const payload = JSON.stringify(serializeCluster(cluster), null, 2);
|
const payload = JSON.stringify(serializeCluster(cluster), null, 2);
|
||||||
const request = `${endpoint}\n${payload}`;
|
const request = `${endpoint}\n${payload}`;
|
||||||
|
|
|
@ -10,3 +10,10 @@ export { validateProxy } from './validate_proxy';
|
||||||
export { validateSeeds } from './validate_seeds';
|
export { validateSeeds } from './validate_seeds';
|
||||||
export { validateSeed } from './validate_seed';
|
export { validateSeed } from './validate_seed';
|
||||||
export { validateServerName } from './validate_server_name';
|
export { validateServerName } from './validate_server_name';
|
||||||
|
export { validateCluster, ClusterErrors } from './validate_cluster';
|
||||||
|
export {
|
||||||
|
isCloudUrlEnabled,
|
||||||
|
validateCloudUrl,
|
||||||
|
convertProxyConnectionToCloudUrl,
|
||||||
|
convertCloudUrlToProxyConnection,
|
||||||
|
} from './validate_cloud_url';
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
isCloudUrlEnabled,
|
||||||
|
validateCloudUrl,
|
||||||
|
convertCloudUrlToProxyConnection,
|
||||||
|
convertProxyConnectionToCloudUrl,
|
||||||
|
i18nTexts,
|
||||||
|
} from './validate_cloud_url';
|
||||||
|
|
||||||
|
describe('Cloud url', () => {
|
||||||
|
describe('validation', () => {
|
||||||
|
it('errors when the url is empty', () => {
|
||||||
|
const actual = validateCloudUrl('');
|
||||||
|
expect(actual).toBe(i18nTexts.urlEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when the url is invalid', () => {
|
||||||
|
const actual = validateCloudUrl('invalid%url');
|
||||||
|
expect(actual).toBe(i18nTexts.urlInvalid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('is cloud url', () => {
|
||||||
|
it('true for a new cluster', () => {
|
||||||
|
const actual = isCloudUrlEnabled();
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('true when proxy connection is empty', () => {
|
||||||
|
const actual = isCloudUrlEnabled({ name: 'test', proxyAddress: '', serverName: '' });
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('true when proxy address is the same as server name and default port', () => {
|
||||||
|
const actual = isCloudUrlEnabled({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'some-proxy:9400',
|
||||||
|
serverName: 'some-proxy',
|
||||||
|
});
|
||||||
|
expect(actual).toBe(true);
|
||||||
|
});
|
||||||
|
it('false when proxy address is the same as server name but not default port', () => {
|
||||||
|
const actual = isCloudUrlEnabled({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'some-proxy:1234',
|
||||||
|
serverName: 'some-proxy',
|
||||||
|
});
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
it('true when proxy address is not the same as server name', () => {
|
||||||
|
const actual = isCloudUrlEnabled({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'some-proxy:9400',
|
||||||
|
serverName: 'some-server-name',
|
||||||
|
});
|
||||||
|
expect(actual).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('conversion from cloud url', () => {
|
||||||
|
it('empty url to empty proxy connection values', () => {
|
||||||
|
const actual = convertCloudUrlToProxyConnection('');
|
||||||
|
expect(actual).toEqual({ proxyAddress: '', serverName: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('url with protocol and port to proxy connection values', () => {
|
||||||
|
const actual = convertCloudUrlToProxyConnection('http://test.com:1234');
|
||||||
|
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('url with protocol and no port to proxy connection values', () => {
|
||||||
|
const actual = convertCloudUrlToProxyConnection('http://test.com');
|
||||||
|
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('url with no protocol to proxy connection values', () => {
|
||||||
|
const actual = convertCloudUrlToProxyConnection('test.com');
|
||||||
|
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
|
||||||
|
});
|
||||||
|
it('invalid url to empty proxy connection values', () => {
|
||||||
|
const actual = convertCloudUrlToProxyConnection('invalid%url');
|
||||||
|
expect(actual).toEqual({ proxyAddress: '', serverName: '' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('conversion to cloud url', () => {
|
||||||
|
it('empty proxy address to empty cloud url', () => {
|
||||||
|
const actual = convertProxyConnectionToCloudUrl({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: '',
|
||||||
|
serverName: 'test',
|
||||||
|
});
|
||||||
|
expect(actual).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty server name to empty cloud url', () => {
|
||||||
|
const actual = convertProxyConnectionToCloudUrl({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'test',
|
||||||
|
serverName: '',
|
||||||
|
});
|
||||||
|
expect(actual).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('different proxy address and server name to empty cloud url', () => {
|
||||||
|
const actual = convertProxyConnectionToCloudUrl({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'test',
|
||||||
|
serverName: 'another-test',
|
||||||
|
});
|
||||||
|
expect(actual).toEqual('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('valid proxy connection to cloud url', () => {
|
||||||
|
const actual = convertProxyConnectionToCloudUrl({
|
||||||
|
name: 'test',
|
||||||
|
proxyAddress: 'test-proxy:9400',
|
||||||
|
serverName: 'test-proxy',
|
||||||
|
});
|
||||||
|
expect(actual).toEqual('test-proxy');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { Cluster } from '../../../../../../common/lib';
|
||||||
|
import { isAddressValid } from './validate_address';
|
||||||
|
|
||||||
|
export const i18nTexts = {
|
||||||
|
urlEmpty: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.cloudDeploymentForm.urlRequiredError"
|
||||||
|
defaultMessage="A url is required."
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
urlInvalid: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.cloudDeploymentForm.urlInvalidError"
|
||||||
|
defaultMessage="Url is invalid"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLOUD_DEFAULT_PROXY_PORT = '9400';
|
||||||
|
const EMPTY_PROXY_VALUES = { proxyAddress: '', serverName: '' };
|
||||||
|
const PROTOCOL_REGEX = new RegExp(/^https?:\/\//);
|
||||||
|
|
||||||
|
export const isCloudUrlEnabled = (cluster?: Cluster): boolean => {
|
||||||
|
// enable cloud url for new clusters
|
||||||
|
if (!cluster) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { proxyAddress, serverName } = cluster;
|
||||||
|
if (!proxyAddress && !serverName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const portParts = (proxyAddress ?? '').split(':');
|
||||||
|
const proxyAddressWithoutPort = portParts[0];
|
||||||
|
const port = portParts[1];
|
||||||
|
return port === CLOUD_DEFAULT_PROXY_PORT && proxyAddressWithoutPort === serverName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUrl = (url: string) => {
|
||||||
|
url = (url ?? '').trim().toLowerCase();
|
||||||
|
// delete http(s):// protocol string if any
|
||||||
|
url = url.replace(PROTOCOL_REGEX, '');
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertProxyConnectionToCloudUrl = (cluster?: Cluster): string => {
|
||||||
|
if (!isCloudUrlEnabled(cluster)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return cluster?.serverName ?? '';
|
||||||
|
};
|
||||||
|
export const convertCloudUrlToProxyConnection = (
|
||||||
|
cloudUrl: string = ''
|
||||||
|
): { proxyAddress: string; serverName: string } => {
|
||||||
|
cloudUrl = formatUrl(cloudUrl);
|
||||||
|
if (!cloudUrl || !isAddressValid(cloudUrl)) {
|
||||||
|
return EMPTY_PROXY_VALUES;
|
||||||
|
}
|
||||||
|
const address = cloudUrl.split(':')[0];
|
||||||
|
return { proxyAddress: `${address}:${CLOUD_DEFAULT_PROXY_PORT}`, serverName: address };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateCloudUrl = (cloudUrl: string): JSX.Element | null => {
|
||||||
|
if (!cloudUrl) {
|
||||||
|
return i18nTexts.urlEmpty;
|
||||||
|
}
|
||||||
|
cloudUrl = formatUrl(cloudUrl);
|
||||||
|
if (!isAddressValid(cloudUrl)) {
|
||||||
|
return i18nTexts.urlInvalid;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { validateName } from './validate_name';
|
||||||
|
import { PROXY_MODE, SNIFF_MODE } from '../../../../../../common/constants';
|
||||||
|
import { validateSeeds } from './validate_seeds';
|
||||||
|
import { validateProxy } from './validate_proxy';
|
||||||
|
import { validateServerName } from './validate_server_name';
|
||||||
|
import { validateCloudUrl } from './validate_cloud_url';
|
||||||
|
import { FormFields } from '../remote_cluster_form';
|
||||||
|
|
||||||
|
type ClusterError = JSX.Element | null;
|
||||||
|
|
||||||
|
export interface ClusterErrors {
|
||||||
|
name?: ClusterError;
|
||||||
|
seeds?: ClusterError;
|
||||||
|
proxyAddress?: ClusterError;
|
||||||
|
serverName?: ClusterError;
|
||||||
|
cloudUrl?: ClusterError;
|
||||||
|
}
|
||||||
|
export const validateCluster = (fields: FormFields, isCloudEnabled: boolean): ClusterErrors => {
|
||||||
|
const { name, seeds = [], mode, proxyAddress, serverName, cloudUrlEnabled, cloudUrl } = fields;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: validateName(name),
|
||||||
|
seeds: mode === SNIFF_MODE ? validateSeeds(seeds) : null,
|
||||||
|
proxyAddress: !cloudUrlEnabled && mode === PROXY_MODE ? validateProxy(proxyAddress) : null,
|
||||||
|
// server name is only required in cloud when proxy mode is enabled
|
||||||
|
serverName:
|
||||||
|
!cloudUrlEnabled && isCloudEnabled && mode === PROXY_MODE
|
||||||
|
? validateServerName(serverName)
|
||||||
|
: null,
|
||||||
|
cloudUrl: cloudUrlEnabled ? validateCloudUrl(cloudUrl) : null,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
|
|
||||||
import { isAddressValid, isPortValid } from './validate_address';
|
|
||||||
|
|
||||||
export function validateSeed(seed?: string): string[] {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (!seed) {
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = isAddressValid(seed);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
errors.push(
|
|
||||||
i18n.translate(
|
|
||||||
'xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage',
|
|
||||||
{
|
|
||||||
defaultMessage:
|
|
||||||
'Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. ' +
|
|
||||||
'Hosts can only consist of letters, numbers, and dashes.',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPortValid(seed)) {
|
|
||||||
errors.push(
|
|
||||||
i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage', {
|
|
||||||
defaultMessage: 'A port is required.',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { isAddressValid, isPortValid } from './validate_address';
|
||||||
|
|
||||||
|
export function validateSeed(seed?: string): JSX.Element[] {
|
||||||
|
const errors: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (!seed) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = isAddressValid(seed);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
errors.push(
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage"
|
||||||
|
defaultMessage="Seed node must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPortValid(seed)) {
|
||||||
|
errors.push(
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage"
|
||||||
|
defaultMessage="A port is required."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
11
x-pack/plugins/remote_clusters/public/application/sections/index.d.ts
vendored
Normal file
11
x-pack/plugins/remote_clusters/public/application/sections/index.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
export declare const RemoteClusterEdit: ComponentType;
|
||||||
|
export declare const RemoteClusterAdd: ComponentType;
|
||||||
|
export declare const RemoteClusterList: ComponentType;
|
|
@ -73,7 +73,7 @@ export class RemoteClusterAdd extends PureComponent {
|
||||||
description={
|
description={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.remoteClusters.remoteClustersDescription"
|
id="xpack.remoteClusters.remoteClustersDescription"
|
||||||
defaultMessage="Add a remote cluster that connects to seed nodes or a single proxy address."
|
defaultMessage="Add a remote cluster that connects to seed nodes or to a single proxy address."
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,10 +27,6 @@ import { getRouter, redirect } from '../../services';
|
||||||
import { setBreadcrumbs } from '../../services/breadcrumb';
|
import { setBreadcrumbs } from '../../services/breadcrumb';
|
||||||
import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
|
import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
|
||||||
|
|
||||||
const disabledFields = {
|
|
||||||
name: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RemoteClusterEdit extends Component {
|
export class RemoteClusterEdit extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
|
@ -202,8 +198,7 @@ export class RemoteClusterEdit extends Component {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<RemoteClusterForm
|
<RemoteClusterForm
|
||||||
fields={cluster}
|
cluster={cluster}
|
||||||
disabledFields={disabledFields}
|
|
||||||
isSaving={isEditingCluster}
|
isSaving={isEditingCluster}
|
||||||
saveError={getEditClusterError}
|
saveError={getEditClusterError}
|
||||||
save={this.save}
|
save={this.save}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { trackUserRequest } from './ui_metric';
|
||||||
import { sendGet, sendPost, sendPut, sendDelete, SendGetOptions } from './http';
|
import { sendGet, sendPost, sendPut, sendDelete, SendGetOptions } from './http';
|
||||||
import { Cluster } from '../../../common/lib';
|
import { Cluster } from '../../../common/lib';
|
||||||
|
|
||||||
export async function loadClusters(options: SendGetOptions) {
|
export async function loadClusters(options?: SendGetOptions) {
|
||||||
return await sendGet(undefined, options);
|
return await sendGet(undefined, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ export let proxyModeUrl: string;
|
||||||
export let proxySettingsUrl: string;
|
export let proxySettingsUrl: string;
|
||||||
|
|
||||||
export function init({ links }: DocLinksStart): void {
|
export function init({ links }: DocLinksStart): void {
|
||||||
skippingDisconnectedClustersUrl = `${links.ccs.skippingDisconnectedClusters}`;
|
skippingDisconnectedClustersUrl = links.ccs.skippingDisconnectedClusters;
|
||||||
remoteClustersUrl = `${links.elasticsearch.remoteClusters}`;
|
remoteClustersUrl = links.elasticsearch.remoteClusters;
|
||||||
transportPortUrl = `${links.elasticsearch.transportSettings}`;
|
transportPortUrl = links.elasticsearch.transportSettings;
|
||||||
proxyModeUrl = `${links.elasticsearch.remoteClustersProxy}`;
|
proxyModeUrl = links.elasticsearch.remoteClustersProxy;
|
||||||
proxySettingsUrl = `${links.elasticsearch.remoteClusersProxySettings}`;
|
proxySettingsUrl = links.elasticsearch.remoteClusersProxySettings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,12 @@ export { setBreadcrumbs } from './breadcrumb';
|
||||||
|
|
||||||
export { redirect } from './redirect';
|
export { redirect } from './redirect';
|
||||||
|
|
||||||
export { setUserHasLeftApp, getUserHasLeftApp, registerRouter, getRouter } from './routing';
|
export {
|
||||||
|
setUserHasLeftApp,
|
||||||
|
getUserHasLeftApp,
|
||||||
|
registerRouter,
|
||||||
|
getRouter,
|
||||||
|
AppRouter,
|
||||||
|
} from './routing';
|
||||||
|
|
||||||
export { trackUiMetric, METRIC_TYPE } from './ui_metric';
|
export { trackUiMetric, METRIC_TYPE } from './ui_metric';
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function getUserHasLeftApp() {
|
||||||
return _userHasLeftApp;
|
return _userHasLeftApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppRouter {
|
export interface AppRouter {
|
||||||
history: ScopedHistory;
|
history: ScopedHistory;
|
||||||
route: { location: ScopedHistory['location'] };
|
route: { location: ScopedHistory['location'] };
|
||||||
}
|
}
|
||||||
|
|
11
x-pack/plugins/remote_clusters/public/application/store/index.d.ts
vendored
Normal file
11
x-pack/plugins/remote_clusters/public/application/store/index.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
export declare const remoteClustersStore: Store;
|
||||||
|
export declare const createRemoteClustersStore: () => Store;
|
Binary file not shown.
After Width: | Height: | Size: 192 KiB |
|
@ -60,13 +60,14 @@ export class RemoteClustersUIPlugin
|
||||||
initNotification(toasts, fatalErrors);
|
initNotification(toasts, fatalErrors);
|
||||||
initHttp(http);
|
initHttp(http);
|
||||||
|
|
||||||
const isCloudEnabled = Boolean(cloud?.isCloudEnabled);
|
const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled);
|
||||||
|
const cloudBaseUrl: string = cloud?.baseUrl ?? '';
|
||||||
|
|
||||||
const { renderApp } = await import('./application');
|
const { renderApp } = await import('./application');
|
||||||
const unmountAppCallback = await renderApp(
|
const unmountAppCallback = await renderApp(
|
||||||
element,
|
element,
|
||||||
i18nContext,
|
i18nContext,
|
||||||
{ isCloudEnabled },
|
{ isCloudEnabled, cloudBaseUrl },
|
||||||
history
|
history
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
"declarationMap": true
|
"declarationMap": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
"__jest__/**/*",
|
||||||
"common/**/*",
|
"common/**/*",
|
||||||
"fixtures/**/*",
|
"fixtures/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
"server/**/*",
|
"server/**/*",
|
||||||
|
"../../../typings/**/*",
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src/core/tsconfig.json" },
|
{ "path": "../../../src/core/tsconfig.json" },
|
||||||
|
|
|
@ -16761,10 +16761,6 @@
|
||||||
"xpack.remoteClusters.addTitle": "リモートクラスターを追加",
|
"xpack.remoteClusters.addTitle": "リモートクラスターを追加",
|
||||||
"xpack.remoteClusters.appName": "リモートクラスター",
|
"xpack.remoteClusters.appName": "リモートクラスター",
|
||||||
"xpack.remoteClusters.appTitle": "リモートクラスター",
|
"xpack.remoteClusters.appTitle": "リモートクラスター",
|
||||||
"xpack.remoteClusters.cloudClusterInformationDescription": "クラスターのプロキシアドレスとサーバー名を見つけるには、デプロイメニューの{security}ページに移動し、{searchString}を検索します。",
|
|
||||||
"xpack.remoteClusters.cloudClusterInformationTitle": "Elasticsearch Cloudデプロイのプロキシモードを使用",
|
|
||||||
"xpack.remoteClusters.cloudClusterSearchDescription": "リモートクラスターパラメーター",
|
|
||||||
"xpack.remoteClusters.cloudClusterSecurityDescription": "セキュリティ",
|
|
||||||
"xpack.remoteClusters.configuredByNodeWarningTitle": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。",
|
"xpack.remoteClusters.configuredByNodeWarningTitle": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。",
|
||||||
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "接続済み",
|
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "接続済み",
|
||||||
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続",
|
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続",
|
||||||
|
@ -16838,7 +16834,6 @@
|
||||||
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "サーバー名",
|
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "サーバー名",
|
||||||
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "リモートクラスターごとに開くソケット接続の数。",
|
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "リモートクラスターごとに開くソケット接続の数。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "リクエストを非表示",
|
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "リクエストを非表示",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "「シードノード」フィールドが無効です。",
|
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "「名前」フィールドが無効です。",
|
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "「名前」フィールドが無効です。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "「プロキシアドレス」フィールドが無効です。",
|
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "「プロキシアドレス」フィールドが無効です。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "「シードノード」フィールドが無効です。",
|
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "「シードノード」フィールドが無効です。",
|
||||||
|
|
|
@ -16987,10 +16987,6 @@
|
||||||
"xpack.remoteClusters.addTitle": "添加远程集群",
|
"xpack.remoteClusters.addTitle": "添加远程集群",
|
||||||
"xpack.remoteClusters.appName": "远程集群",
|
"xpack.remoteClusters.appName": "远程集群",
|
||||||
"xpack.remoteClusters.appTitle": "远程集群",
|
"xpack.remoteClusters.appTitle": "远程集群",
|
||||||
"xpack.remoteClusters.cloudClusterInformationDescription": "要查找您的集群的代理地址和服务器名称,请前往部署菜单的{security}页面并搜索“{searchString}”。",
|
|
||||||
"xpack.remoteClusters.cloudClusterInformationTitle": "将代理模式用于 Elastic Cloud 部署",
|
|
||||||
"xpack.remoteClusters.cloudClusterSearchDescription": "远程集群参数",
|
|
||||||
"xpack.remoteClusters.cloudClusterSecurityDescription": "安全",
|
|
||||||
"xpack.remoteClusters.configuredByNodeWarningTitle": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。",
|
"xpack.remoteClusters.configuredByNodeWarningTitle": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。",
|
||||||
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "已连接",
|
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "已连接",
|
||||||
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接",
|
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接",
|
||||||
|
@ -17065,7 +17061,6 @@
|
||||||
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "服务器名",
|
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "服务器名",
|
||||||
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "每个远程集群要打开的套接字数目。",
|
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "每个远程集群要打开的套接字数目。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "隐藏请求",
|
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "隐藏请求",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "“种子节点”字段无效。",
|
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "“名称”字段无效。",
|
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "“名称”字段无效。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "“代理地址”字段无效。",
|
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "“代理地址”字段无效。",
|
||||||
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "“种子节点”字段无效。",
|
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "“种子节点”字段无效。",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue