mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -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.
|
||||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import sinon, { SinonFakeServer } from 'sinon';
|
||||
import { Cluster } from '../../../common/lib';
|
||||
|
||||
// Register helpers to mock HTTP Requests
|
||||
const registerHttpRequestMockHelpers = (server) => {
|
||||
const mockResponse = (response) => [
|
||||
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||
const mockResponse = (response: Cluster[] | { itemsDeleted: string[]; errors: string[] }) => [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify(response),
|
||||
];
|
||||
|
||||
const setLoadRemoteClustersResponse = (response) => {
|
||||
server.respondWith('GET', '/api/remote_clusters', [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify(response),
|
||||
]);
|
||||
const setLoadRemoteClustersResponse = (response: Cluster[] = []) => {
|
||||
server.respondWith('GET', '/api/remote_clusters', mockResponse(response));
|
||||
};
|
||||
|
||||
const setDeleteRemoteClusterResponse = (response) => {
|
||||
const setDeleteRemoteClusterResponse = (
|
||||
response: { itemsDeleted: string[]; errors: string[] } = { itemsDeleted: [], errors: [] }
|
||||
) => {
|
||||
server.respondWith('DELETE', /api\/remote_clusters/, mockResponse(response));
|
||||
};
|
||||
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { nextTick, getRandomString, findTestSubject } from '@kbn/test/jest';
|
||||
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,
|
||||
fatalErrorsServiceMock.createSetupContract()
|
||||
);
|
||||
// This expects HttpSetup but we're giving it AxiosInstance.
|
||||
// @ts-ignore
|
||||
initHttp(mockHttpClient);
|
||||
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
export interface Context {
|
||||
isCloudEnabled: boolean;
|
||||
cloudBaseUrl: string;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<Context>({} as any);
|
||||
|
@ -22,3 +23,10 @@ export const AppContextProvider = ({
|
|||
}) => {
|
||||
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,
|
||||
I18nContext: I18nStart['Context'],
|
||||
appDependencies: {
|
||||
isCloudEnabled?: boolean;
|
||||
isCloudEnabled: boolean;
|
||||
cloudBaseUrl: string;
|
||||
},
|
||||
history: ScopedHistory
|
||||
) => 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 {
|
||||
close: () => void;
|
||||
name: string;
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class RequestFlyout extends PureComponent<Props> {
|
||||
render() {
|
||||
const { name, close, cluster } = this.props;
|
||||
const { close, cluster } = this.props;
|
||||
const { name } = cluster;
|
||||
const endpoint = 'PUT _cluster/settings';
|
||||
const payload = JSON.stringify(serializeCluster(cluster), null, 2);
|
||||
const request = `${endpoint}\n${payload}`;
|
||||
|
|
|
@ -10,3 +10,10 @@ export { validateProxy } from './validate_proxy';
|
|||
export { validateSeeds } from './validate_seeds';
|
||||
export { validateSeed } from './validate_seed';
|
||||
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={
|
||||
<FormattedMessage
|
||||
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 { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
|
||||
|
||||
const disabledFields = {
|
||||
name: true,
|
||||
};
|
||||
|
||||
export class RemoteClusterEdit extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
|
@ -202,8 +198,7 @@ export class RemoteClusterEdit extends Component {
|
|||
</>
|
||||
) : null}
|
||||
<RemoteClusterForm
|
||||
fields={cluster}
|
||||
disabledFields={disabledFields}
|
||||
cluster={cluster}
|
||||
isSaving={isEditingCluster}
|
||||
saveError={getEditClusterError}
|
||||
save={this.save}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { trackUserRequest } from './ui_metric';
|
|||
import { sendGet, sendPost, sendPut, sendDelete, SendGetOptions } from './http';
|
||||
import { Cluster } from '../../../common/lib';
|
||||
|
||||
export async function loadClusters(options: SendGetOptions) {
|
||||
export async function loadClusters(options?: SendGetOptions) {
|
||||
return await sendGet(undefined, options);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ export let proxyModeUrl: string;
|
|||
export let proxySettingsUrl: string;
|
||||
|
||||
export function init({ links }: DocLinksStart): void {
|
||||
skippingDisconnectedClustersUrl = `${links.ccs.skippingDisconnectedClusters}`;
|
||||
remoteClustersUrl = `${links.elasticsearch.remoteClusters}`;
|
||||
transportPortUrl = `${links.elasticsearch.transportSettings}`;
|
||||
proxyModeUrl = `${links.elasticsearch.remoteClustersProxy}`;
|
||||
proxySettingsUrl = `${links.elasticsearch.remoteClusersProxySettings}`;
|
||||
skippingDisconnectedClustersUrl = links.ccs.skippingDisconnectedClusters;
|
||||
remoteClustersUrl = links.elasticsearch.remoteClusters;
|
||||
transportPortUrl = links.elasticsearch.transportSettings;
|
||||
proxyModeUrl = links.elasticsearch.remoteClustersProxy;
|
||||
proxySettingsUrl = links.elasticsearch.remoteClusersProxySettings;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@ export { setBreadcrumbs } from './breadcrumb';
|
|||
|
||||
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';
|
||||
|
|
|
@ -21,7 +21,7 @@ export function getUserHasLeftApp() {
|
|||
return _userHasLeftApp;
|
||||
}
|
||||
|
||||
interface AppRouter {
|
||||
export interface AppRouter {
|
||||
history: ScopedHistory;
|
||||
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);
|
||||
initHttp(http);
|
||||
|
||||
const isCloudEnabled = Boolean(cloud?.isCloudEnabled);
|
||||
const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled);
|
||||
const cloudBaseUrl: string = cloud?.baseUrl ?? '';
|
||||
|
||||
const { renderApp } = await import('./application');
|
||||
const unmountAppCallback = await renderApp(
|
||||
element,
|
||||
i18nContext,
|
||||
{ isCloudEnabled },
|
||||
{ isCloudEnabled, cloudBaseUrl },
|
||||
history
|
||||
);
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"__jest__/**/*",
|
||||
"common/**/*",
|
||||
"fixtures/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
|
|
|
@ -16761,10 +16761,6 @@
|
|||
"xpack.remoteClusters.addTitle": "リモートクラスターを追加",
|
||||
"xpack.remoteClusters.appName": "リモートクラスター",
|
||||
"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.connectedStatus.connectedAriaLabel": "接続済み",
|
||||
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続",
|
||||
|
@ -16838,7 +16834,6 @@
|
|||
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "サーバー名",
|
||||
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "リモートクラスターごとに開くソケット接続の数。",
|
||||
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "リクエストを非表示",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "「シードノード」フィールドが無効です。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "「名前」フィールドが無効です。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "「プロキシアドレス」フィールドが無効です。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "「シードノード」フィールドが無効です。",
|
||||
|
|
|
@ -16987,10 +16987,6 @@
|
|||
"xpack.remoteClusters.addTitle": "添加远程集群",
|
||||
"xpack.remoteClusters.appName": "远程集群",
|
||||
"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.connectedStatus.connectedAriaLabel": "已连接",
|
||||
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接",
|
||||
|
@ -17065,7 +17061,6 @@
|
|||
"xpack.remoteClusters.remoteClusterForm.fieldServerNameRequiredLabel": "服务器名",
|
||||
"xpack.remoteClusters.remoteClusterForm.fieldSocketConnectionsHelpText": "每个远程集群要打开的套接字数目。",
|
||||
"xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel": "隐藏请求",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage": "“种子节点”字段无效。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage": "“名称”字段无效。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage": "“代理地址”字段无效。",
|
||||
"xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage": "“种子节点”字段无效。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue