[8.x] [Remote Cluster] Improve UX (#206958) (#208584)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Remote Cluster] Improve UX
(#206958)](https://github.com/elastic/kibana/pull/206958)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Sonia Sanz
Vivas","email":"sonia.sanzvivas@elastic.co"},"sourceCommit":{"committedDate":"2025-01-28T16:55:32Z","message":"[Remote
Cluster] Improve UX (#206958)\n\nCloses
https://github.com/elastic/kibana/issues/199664\nPart of
https://github.com/elastic/kibana/issues/205027 (removes
.scss\nfile)\n## Summary\n\n<details>\n
<summary>Screenshots</summary>\n\n **Add remote cluster in
Cloud**\n\n<img width=\"1512\" alt=\"Screenshot 2025-01-28 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/b8cf24be-8e04-4629-939c-c8d781a727e4\"\n/>\n\n<img
width=\"1500\" alt=\"Screenshot 2025-01-28 at 10 36
03\"\nsrc=\"https://github.com/user-attachments/assets/612dbc29-f5cb-4809-a4e7-95a99cd2643a\"\n/>\n\n\n**Cloud/Certificate**\n\n<img
width=\"1502\" alt=\"Screenshot 2025-01-28 at 10 36
39\"\nsrc=\"https://github.com/user-attachments/assets/b174735f-41ce-4fdb-872e-5db26e3400e0\"\n/>\n\n**Cloud/API**\n\n<img
width=\"1505\" alt=\"Screenshot 2025-01-28 at 10 36
28\"\nsrc=\"https://github.com/user-attachments/assets/c6c03589-1bf8-4e47-8c8d-2ab8da897c4e\"\n/>\n\n**Edit
remote cluster in Cloud**\n\n<img width=\"1506\" alt=\"Screenshot
2025-01-28 at 10 37
47\"\nsrc=\"https://github.com/user-attachments/assets/bc74e3fb-28e8-41ca-90d9-c3b09c9b8532\"\n/>\n\n**Add
remote cluster in Stateful/API Key and Cert**\n\n<img width=\"1501\"
alt=\"Screenshot 2025-01-28 at 10 31
36\"\nsrc=\"https://github.com/user-attachments/assets/2ed78cc3-2d5a-4090-9b3d-fd6c78b73c00\"\n/>\n<img
width=\"1503\" alt=\"Screenshot 2025-01-28 at 10 32
02\"\nsrc=\"https://github.com/user-attachments/assets/33487816-613e-4e8b-8ff4-dfb73ff4c67c\"\n/>\n<img
width=\"756\" alt=\"Screenshot 2025-01-28 at 10 32
13\"\nsrc=\"https://github.com/user-attachments/assets/e387f6e8-51f0-4b8e-859d-88f5112897f9\"\n/>\n\n**Edit
remote cluster in Stateful**\n\n<img width=\"1505\" alt=\"Screenshot
2025-01-28 at 10 31
08\"\nsrc=\"https://github.com/user-attachments/assets/e176dd9e-0336-412e-a231-a07f92a6ae0f\"\n/>\n</details>\n\n###
About this PR\n\n#### Typescript\nAll files in this PR have been created
in typescript or moved to\ntypescript. It has been intended that
everything follows strict typing\nbut in two cases:
`RemoteClusterAdd.container` and\n`RemoteClusterEdit.container`. In
these cases it has been chosen to add\ntype `any()`, which is basically
the same as it already existed when\nthey were `.js` files. The reason
for not adding strict typing to these\nfiles is that it would add more
noise to this PR and is something that\ncan be done as a
follow-up.\n\nNo files were migrated to typescript that did not directly
affect the\nchanges within the scope of this PR.\n\n#### Styles\nI
removed `_hacks.scss`. The only class that was in use
was\n`remoteClusterSkipIfUnavailableSwitch`, and only the
`padding-top`\napplied. I moved this padding to `emotion` in the
`remote_cluster_form`\nfile, using `euiTheme`.\n\nI realized that the
plugin has some `classNames` in other files that\ndoesn't exist in any
style file. I'm pretty sure it's dead code and can\nbe removed in a
follow-up.\n\n#### Possible follow-ups\n* Remove dead css classes\n* Add
types to `RemoteClusterAdd.container`
and\n`RemoteClusterEdit.container`\n* Migrate the rest of the files in
the plugin to typescript\n* Move RequestFlyout
to\n[ViewApiRequestFlyout](cfb1997d7b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx (L55))\n*
Add Functional test that actually connects the clusters\n\n### How to
test\n\n### Checklist\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests
changed\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7776","sha":"7b5d6031023d30ddf9e2080222036c1048c73366","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:CCR
and Remote Clusters","Team:Kibana
Management","release_note:skip","v9.0.0","backport:prev-minor","ci:cloud-deploy"],"title":"[Remote
Cluster] Improve
UX","number":206958,"url":"https://github.com/elastic/kibana/pull/206958","mergeCommit":{"message":"[Remote
Cluster] Improve UX (#206958)\n\nCloses
https://github.com/elastic/kibana/issues/199664\nPart of
https://github.com/elastic/kibana/issues/205027 (removes
.scss\nfile)\n## Summary\n\n<details>\n
<summary>Screenshots</summary>\n\n **Add remote cluster in
Cloud**\n\n<img width=\"1512\" alt=\"Screenshot 2025-01-28 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/b8cf24be-8e04-4629-939c-c8d781a727e4\"\n/>\n\n<img
width=\"1500\" alt=\"Screenshot 2025-01-28 at 10 36
03\"\nsrc=\"https://github.com/user-attachments/assets/612dbc29-f5cb-4809-a4e7-95a99cd2643a\"\n/>\n\n\n**Cloud/Certificate**\n\n<img
width=\"1502\" alt=\"Screenshot 2025-01-28 at 10 36
39\"\nsrc=\"https://github.com/user-attachments/assets/b174735f-41ce-4fdb-872e-5db26e3400e0\"\n/>\n\n**Cloud/API**\n\n<img
width=\"1505\" alt=\"Screenshot 2025-01-28 at 10 36
28\"\nsrc=\"https://github.com/user-attachments/assets/c6c03589-1bf8-4e47-8c8d-2ab8da897c4e\"\n/>\n\n**Edit
remote cluster in Cloud**\n\n<img width=\"1506\" alt=\"Screenshot
2025-01-28 at 10 37
47\"\nsrc=\"https://github.com/user-attachments/assets/bc74e3fb-28e8-41ca-90d9-c3b09c9b8532\"\n/>\n\n**Add
remote cluster in Stateful/API Key and Cert**\n\n<img width=\"1501\"
alt=\"Screenshot 2025-01-28 at 10 31
36\"\nsrc=\"https://github.com/user-attachments/assets/2ed78cc3-2d5a-4090-9b3d-fd6c78b73c00\"\n/>\n<img
width=\"1503\" alt=\"Screenshot 2025-01-28 at 10 32
02\"\nsrc=\"https://github.com/user-attachments/assets/33487816-613e-4e8b-8ff4-dfb73ff4c67c\"\n/>\n<img
width=\"756\" alt=\"Screenshot 2025-01-28 at 10 32
13\"\nsrc=\"https://github.com/user-attachments/assets/e387f6e8-51f0-4b8e-859d-88f5112897f9\"\n/>\n\n**Edit
remote cluster in Stateful**\n\n<img width=\"1505\" alt=\"Screenshot
2025-01-28 at 10 31
08\"\nsrc=\"https://github.com/user-attachments/assets/e176dd9e-0336-412e-a231-a07f92a6ae0f\"\n/>\n</details>\n\n###
About this PR\n\n#### Typescript\nAll files in this PR have been created
in typescript or moved to\ntypescript. It has been intended that
everything follows strict typing\nbut in two cases:
`RemoteClusterAdd.container` and\n`RemoteClusterEdit.container`. In
these cases it has been chosen to add\ntype `any()`, which is basically
the same as it already existed when\nthey were `.js` files. The reason
for not adding strict typing to these\nfiles is that it would add more
noise to this PR and is something that\ncan be done as a
follow-up.\n\nNo files were migrated to typescript that did not directly
affect the\nchanges within the scope of this PR.\n\n#### Styles\nI
removed `_hacks.scss`. The only class that was in use
was\n`remoteClusterSkipIfUnavailableSwitch`, and only the
`padding-top`\napplied. I moved this padding to `emotion` in the
`remote_cluster_form`\nfile, using `euiTheme`.\n\nI realized that the
plugin has some `classNames` in other files that\ndoesn't exist in any
style file. I'm pretty sure it's dead code and can\nbe removed in a
follow-up.\n\n#### Possible follow-ups\n* Remove dead css classes\n* Add
types to `RemoteClusterAdd.container`
and\n`RemoteClusterEdit.container`\n* Migrate the rest of the files in
the plugin to typescript\n* Move RequestFlyout
to\n[ViewApiRequestFlyout](cfb1997d7b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx (L55))\n*
Add Functional test that actually connects the clusters\n\n### How to
test\n\n### Checklist\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests
changed\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7776","sha":"7b5d6031023d30ddf9e2080222036c1048c73366"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206958","number":206958,"mergeCommit":{"message":"[Remote
Cluster] Improve UX (#206958)\n\nCloses
https://github.com/elastic/kibana/issues/199664\nPart of
https://github.com/elastic/kibana/issues/205027 (removes
.scss\nfile)\n## Summary\n\n<details>\n
<summary>Screenshots</summary>\n\n **Add remote cluster in
Cloud**\n\n<img width=\"1512\" alt=\"Screenshot 2025-01-28 at 10 34
23\"\nsrc=\"https://github.com/user-attachments/assets/b8cf24be-8e04-4629-939c-c8d781a727e4\"\n/>\n\n<img
width=\"1500\" alt=\"Screenshot 2025-01-28 at 10 36
03\"\nsrc=\"https://github.com/user-attachments/assets/612dbc29-f5cb-4809-a4e7-95a99cd2643a\"\n/>\n\n\n**Cloud/Certificate**\n\n<img
width=\"1502\" alt=\"Screenshot 2025-01-28 at 10 36
39\"\nsrc=\"https://github.com/user-attachments/assets/b174735f-41ce-4fdb-872e-5db26e3400e0\"\n/>\n\n**Cloud/API**\n\n<img
width=\"1505\" alt=\"Screenshot 2025-01-28 at 10 36
28\"\nsrc=\"https://github.com/user-attachments/assets/c6c03589-1bf8-4e47-8c8d-2ab8da897c4e\"\n/>\n\n**Edit
remote cluster in Cloud**\n\n<img width=\"1506\" alt=\"Screenshot
2025-01-28 at 10 37
47\"\nsrc=\"https://github.com/user-attachments/assets/bc74e3fb-28e8-41ca-90d9-c3b09c9b8532\"\n/>\n\n**Add
remote cluster in Stateful/API Key and Cert**\n\n<img width=\"1501\"
alt=\"Screenshot 2025-01-28 at 10 31
36\"\nsrc=\"https://github.com/user-attachments/assets/2ed78cc3-2d5a-4090-9b3d-fd6c78b73c00\"\n/>\n<img
width=\"1503\" alt=\"Screenshot 2025-01-28 at 10 32
02\"\nsrc=\"https://github.com/user-attachments/assets/33487816-613e-4e8b-8ff4-dfb73ff4c67c\"\n/>\n<img
width=\"756\" alt=\"Screenshot 2025-01-28 at 10 32
13\"\nsrc=\"https://github.com/user-attachments/assets/e387f6e8-51f0-4b8e-859d-88f5112897f9\"\n/>\n\n**Edit
remote cluster in Stateful**\n\n<img width=\"1505\" alt=\"Screenshot
2025-01-28 at 10 31
08\"\nsrc=\"https://github.com/user-attachments/assets/e176dd9e-0336-412e-a231-a07f92a6ae0f\"\n/>\n</details>\n\n###
About this PR\n\n#### Typescript\nAll files in this PR have been created
in typescript or moved to\ntypescript. It has been intended that
everything follows strict typing\nbut in two cases:
`RemoteClusterAdd.container` and\n`RemoteClusterEdit.container`. In
these cases it has been chosen to add\ntype `any()`, which is basically
the same as it already existed when\nthey were `.js` files. The reason
for not adding strict typing to these\nfiles is that it would add more
noise to this PR and is something that\ncan be done as a
follow-up.\n\nNo files were migrated to typescript that did not directly
affect the\nchanges within the scope of this PR.\n\n#### Styles\nI
removed `_hacks.scss`. The only class that was in use
was\n`remoteClusterSkipIfUnavailableSwitch`, and only the
`padding-top`\napplied. I moved this padding to `emotion` in the
`remote_cluster_form`\nfile, using `euiTheme`.\n\nI realized that the
plugin has some `classNames` in other files that\ndoesn't exist in any
style file. I'm pretty sure it's dead code and can\nbe removed in a
follow-up.\n\n#### Possible follow-ups\n* Remove dead css classes\n* Add
types to `RemoteClusterAdd.container`
and\n`RemoteClusterEdit.container`\n* Migrate the rest of the files in
the plugin to typescript\n* Move RequestFlyout
to\n[ViewApiRequestFlyout](cfb1997d7b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx (L55))\n*
Add Functional test that actually connects the clusters\n\n### How to
test\n\n### Checklist\n\n- [x] Any text added follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] [Flaky
Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\nused on any tests
changed\nhttps://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7776","sha":"7b5d6031023d30ddf9e2080222036c1048c73366"}}]}]
BACKPORT-->

Co-authored-by: Sonia Sanz Vivas <sonia.sanzvivas@elastic.co>
This commit is contained in:
Kibana Machine 2025-01-29 05:42:38 +11:00 committed by GitHub
parent c1c77285a1
commit 5779a170d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 2034 additions and 1625 deletions

View file

@ -12,6 +12,12 @@ import { setupEnvironment, RemoteClustersActions } from '../helpers';
import { setup } from './remote_clusters_add.helpers';
import { NON_ALPHA_NUMERIC_CHARS, ACCENTED_CHARS } from './special_characters';
import { MAX_NODE_CONNECTIONS } from '../../../common/constants';
import {
onPremPrerequisitesApiKey,
onPremPrerequisitesCert,
onPremSecurityApiKey,
onPremSecurityCert,
} from '../../../public/application/services/documentation';
const notInArray = (array: string[]) => (value: string) => array.indexOf(value) < 0;
@ -38,186 +44,20 @@ describe('Create Remote cluster', () => {
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 "true"
expect(actions.skipUnavailableSwitch.isChecked()).toBe(true);
actions.skipUnavailableSwitch.toggle();
expect(actions.skipUnavailableSwitch.isChecked()).toBe(false);
});
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 advanced options', () => {
expect(actions.cloudAdvancedOptionsSwitch.exists()).toBe(false);
});
});
describe('on cloud', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, { isCloudEnabled: true }));
});
component.update();
});
test('TLS server name has optional label', () => {
actions.cloudAdvancedOptionsSwitch.toggle();
expect(actions.tlsServerNameInput.getLabel()).toBe('TLS server name (optional)');
});
test('renders a switch for advanced options', () => {
expect(actions.cloudAdvancedOptionsSwitch.exists()).toBe(true);
});
test('renders no switch between sniff and proxy modes', () => {
expect(actions.connectionModeSwitch.exists()).toBe(false);
});
test('advanced options are initially disabled', () => {
expect(actions.cloudAdvancedOptionsSwitch.isChecked()).toBe(false);
});
});
});
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(httpSetup));
});
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('Setup Trust', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, {
canUseAPIKeyTrustModel: true,
}));
});
component.update();
actions.nameInput.setValue('remote_cluster_test');
actions.seedsInput.setValue('192.168.1.1:3000');
await actions.saveButton.click();
});
test('should contain two cards for setting up trust', () => {
// Cards exist
expect(actions.setupTrust.apiCardExist()).toBe(true);
expect(actions.setupTrust.certCardExist()).toBe(true);
// Each card has its doc link
expect(actions.setupTrust.apiCardDocsExist()).toBe(true);
expect(actions.setupTrust.certCardDocsExist()).toBe(true);
expect(actions.setupTrustStep.apiCardExist()).toBe(true);
expect(actions.setupTrustStep.certCardExist()).toBe(true);
});
test('on submit should open confirm modal', async () => {
await actions.setupTrust.setupTrustConfirmClick();
expect(actions.setupTrust.isSubmitInConfirmDisabled()).toBe(true);
await actions.setupTrust.toggleConfirmSwitch();
expect(actions.setupTrust.isSubmitInConfirmDisabled()).toBe(false);
test('next button should be disabled if not trust mode selected', async () => {
expect(actions.setupTrustStep.button.isDisabled()).toBe(true);
});
test('back button goes to first step', async () => {
await actions.setupTrust.backToFirstStepClick();
expect(actions.isOnFirstStep()).toBe(true);
test('next button should be enabled if trust mode selected', async () => {
await actions.setupTrustStep.selectApiKeyTrustMode();
expect(actions.setupTrustStep.button.isDisabled()).toBe(false);
});
test('shows only cert based config if API key trust model is not available', async () => {
@ -227,99 +67,339 @@ describe('Create Remote cluster', () => {
}));
});
component.update();
actions.nameInput.setValue('remote_cluster_test');
actions.seedsInput.setValue('192.168.1.1:3000');
await actions.saveButton.click();
expect(actions.setupTrust.apiCardExist()).toBe(false);
expect(actions.setupTrust.certCardExist()).toBe(true);
expect(actions.setupTrustStep.apiCardExist()).toBe(false);
expect(actions.setupTrustStep.certCardExist()).toBe(true);
});
});
describe('on prem', () => {
describe('Form step', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup));
({ actions, component } = await setup(httpSetup, {
canUseAPIKeyTrustModel: true,
}));
});
component.update();
actions.nameInput.setValue('remote_cluster_test');
await actions.setupTrustStep.selectApiKeyTrustMode();
await actions.setupTrustStep.button.click();
});
describe('seeds', () => {
test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => {
await actions.saveButton.click(); // display form errors
test('should have a toggle to Skip unavailable remote cluster', () => {
expect(actions.formStep.skipUnavailableSwitch.exists()).toBe(true);
// By default it should be set to "true"
expect(actions.formStep.skipUnavailableSwitch.isChecked()).toBe(true);
actions.formStep.skipUnavailableSwitch.toggle();
expect(actions.formStep.skipUnavailableSwitch.isChecked()).toBe(false);
});
test('back button goes to first step', async () => {
await actions.formStep.backButton.click();
expect(actions.setupTrustStep.isOnTrustStep()).toBe(true);
});
describe('on prem', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup));
});
component.update();
actions.formStep.nameInput.setValue('remote_cluster_test');
});
test('should have a toggle to enable "proxy" mode for a remote cluster', () => {
expect(actions.formStep.connectionModeSwitch.exists()).toBe(true);
// By default it should be set to "false"
expect(actions.formStep.connectionModeSwitch.isChecked()).toBe(false);
actions.formStep.connectionModeSwitch.toggle();
expect(actions.formStep.connectionModeSwitch.isChecked()).toBe(true);
});
test('server name has optional label', () => {
actions.formStep.connectionModeSwitch.toggle();
expect(actions.formStep.serverNameInput.getLabel()).toBe('Server name (optional)');
});
test('should display errors and disable the next button when clicking "next" without filling the form', async () => {
actions.formStep.nameInput.setValue('');
expect(actions.globalErrorExists()).toBe(false);
expect(actions.formStep.button.isDisabled()).toBe(false);
await actions.formStep.button.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.formStep.button.isDisabled()).toBe(true);
});
test('renders no switch for cloud advanced options', () => {
expect(actions.formStep.cloudAdvancedOptionsSwitch.exists()).toBe(false);
});
describe('seeds', () => {
test('should only allow alpha-numeric characters and "-" (dash) in the node "host" part', async () => {
await actions.formStep.button.click(); // display form errors
const expectInvalidChar = (char: string) => {
actions.formStep.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.formStep.button.click();
actions.formStep.seedsInput.setValue('192.168.1.1');
expect(actions.getErrorMessages()).toContain('A port is required.');
actions.formStep.seedsInput.setValue('192.168.1.1:abc');
expect(actions.getErrorMessages()).toContain('A port is required.');
});
});
describe('node connections', () => {
test('should require a valid number of node connections', async () => {
await actions.formStep.button.click();
actions.formStep.nodeConnectionsInput.setValue(String(MAX_NODE_CONNECTIONS + 1));
expect(actions.getErrorMessages()).toContain(
`This number must be equal or less than ${MAX_NODE_CONNECTIONS}.`
);
});
});
test('server name is optional (proxy connection)', () => {
actions.formStep.connectionModeSwitch.toggle();
actions.formStep.button.click();
expect(actions.getErrorMessages()).toEqual(['A proxy address is required.']);
});
});
describe('on cloud', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, { isCloudEnabled: true }));
});
component.update();
});
test('TLS server name has optional label', () => {
actions.formStep.cloudAdvancedOptionsSwitch.toggle();
expect(actions.formStep.tlsServerNameInput.getLabel()).toBe('TLS server name (optional)');
});
test('renders a switch for advanced options', () => {
expect(actions.formStep.cloudAdvancedOptionsSwitch.exists()).toBe(true);
});
test('renders no switch between sniff and proxy modes', () => {
expect(actions.formStep.connectionModeSwitch.exists()).toBe(false);
});
test('advanced options are initially disabled', () => {
expect(actions.formStep.cloudAdvancedOptionsSwitch.isChecked()).toBe(false);
});
test('remote address is required', () => {
actions.formStep.button.click();
expect(actions.getErrorMessages()).toContain('A remote address is required.');
});
test('should only allow alpha-numeric characters and "-" (dash) in the remote address "host" part', async () => {
await actions.formStep.button.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.`
);
actions.formStep.cloudRemoteAddressInput.setValue(`192.16${char}:3000`);
expect(actions.getErrorMessages()).toContain('Remote address is invalid.');
};
[...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.');
});
});
describe('form validation', () => {
describe('remote cluster name', () => {
test('should not allow spaces', async () => {
actions.formStep.nameInput.setValue('with space');
describe('node connections', () => {
test('should require a valid number of node connections', async () => {
await actions.saveButton.click();
await actions.formStep.button.click();
actions.nodeConnectionsInput.setValue(String(MAX_NODE_CONNECTIONS + 1));
expect(actions.getErrorMessages()).toContain(
`This number must be equal or less than ${MAX_NODE_CONNECTIONS}.`
);
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.formStep.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.formStep.button.click(); // display form errors
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS].forEach(expectInvalidChar);
});
});
});
test('server name is optional (proxy connection)', () => {
actions.connectionModeSwitch.toggle();
actions.saveButton.click();
expect(actions.getErrorMessages()).toEqual(['A proxy address is required.']);
describe('proxy address', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup));
});
component.update();
actions.formStep.connectionModeSwitch.toggle();
});
test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', async () => {
await actions.formStep.button.click(); // display form errors
const expectInvalidChar = (char: string) => {
actions.formStep.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.formStep.button.click();
actions.formStep.proxyAddressInput.setValue('192.168.1.1');
expect(actions.getErrorMessages()).toContain('A port is required.');
actions.formStep.proxyAddressInput.setValue('192.168.1.1:abc');
expect(actions.getErrorMessages()).toContain('A port is required.');
});
});
});
});
describe('on cloud', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, { isCloudEnabled: true }));
describe('Review step', () => {
describe('on cloud', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, { isCloudEnabled: true }));
});
component.update();
});
component.update();
test('back button goes to second step', async () => {
await actions.setupTrustStep.selectApiKeyTrustMode();
await actions.setupTrustStep.button.click();
await actions.formStep.nameInput.setValue('remote_cluster_apiKey_cloud');
await actions.formStep.cloudRemoteAddressInput.setValue('1:1');
await actions.formStep.button.click();
await actions.reviewStep.backButton.click();
expect(actions.formStep.isOnFormStep()).toBe(true);
});
test('shows expected documentation when api_key is selected', async () => {
await actions.setupTrustStep.selectApiKeyTrustMode();
await actions.setupTrustStep.button.click();
await actions.formStep.nameInput.setValue('remote_cluster_apiKey_cloud');
await actions.formStep.cloudRemoteAddressInput.setValue('1:1');
await actions.formStep.button.click();
expect(actions.reviewStep.cloud.apiKeyDocumentationExists()).toBe(true);
expect(actions.reviewStep.cloud.certDocumentationExists()).toBe(false);
});
test('shows expected documentation when cert is selected', async () => {
await actions.setupTrustStep.selectCertificatesTrustMode();
await actions.setupTrustStep.button.click();
await actions.formStep.nameInput.setValue('remote_cluster_cert_cloud');
await actions.formStep.cloudRemoteAddressInput.setValue('1:1');
await actions.formStep.button.click();
expect(actions.reviewStep.cloud.certDocumentationExists()).toBe(true);
expect(actions.reviewStep.cloud.apiKeyDocumentationExists()).toBe(false);
});
});
describe('on prem', () => {
beforeEach(async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup));
});
test('remote address is required', () => {
actions.saveButton.click();
expect(actions.getErrorMessages()).toContain('A remote address is required.');
});
component.update();
});
test('should only allow alpha-numeric characters and "-" (dash) in the remote address "host" part', async () => {
await actions.saveButton.click(); // display form errors
test('shows expected documentation when api_key is selected', async () => {
const open = jest.fn();
global.open = open;
const expectInvalidChar = (char: string) => {
actions.cloudRemoteAddressInput.setValue(`192.16${char}:3000`);
expect(actions.getErrorMessages()).toContain('Remote address is invalid.');
};
await actions.setupTrustStep.selectApiKeyTrustMode();
await actions.setupTrustStep.button.click();
[...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS]
.filter(notInArray(['-', '_', ':']))
.forEach(expectInvalidChar);
await actions.formStep.nameInput.setValue('remote_cluster_apiKey_onPrem');
await actions.formStep.seedsInput.setValue('1:1');
await actions.formStep.button.click();
expect(actions.reviewStep.onPrem.step1LinkExists()).toBe(true);
expect(actions.reviewStep.onPrem.step1Link()).toBe(onPremPrerequisitesApiKey);
expect(actions.reviewStep.onPrem.step2LinkExists()).toBe(true);
expect(actions.reviewStep.onPrem.step2Link()).toBe(onPremSecurityApiKey);
});
test('shows expected documentation when cert is selected', async () => {
const open = jest.fn();
global.open = open;
await actions.setupTrustStep.selectCertificatesTrustMode;
await actions.setupTrustStep.button.click();
await actions.formStep.nameInput.setValue('remote_cluster_cert_onPrem');
await actions.formStep.seedsInput.setValue('1:1');
await actions.formStep.button.click();
expect(actions.reviewStep.onPrem.step1LinkExists()).toBe(true);
expect(actions.reviewStep.onPrem.step1Link()).toBe(onPremPrerequisitesCert);
expect(actions.reviewStep.onPrem.step2LinkExists()).toBe(true);
expect(actions.reviewStep.onPrem.step2Link()).toBe(onPremSecurityCert);
});
});
});
});

View file

@ -8,6 +8,7 @@
import { registerTestBed, TestBedConfig } from '@kbn/test-jest-helpers';
import { HttpSetup } from '@kbn/core/public';
import { SECURITY_MODEL } from '../../../common/constants';
import { Cluster } from '../../../public';
import { RemoteClusterEdit } from '../../../public/application/sections';
import { createRemoteClustersStore } from '../../../public/application/store';
@ -20,7 +21,7 @@ export const REMOTE_CLUSTER_EDIT: Cluster = {
name: REMOTE_CLUSTER_EDIT_NAME,
seeds: ['localhost:9400'],
skipUnavailable: true,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
};
const testBedConfig: TestBedConfig = {

View file

@ -17,6 +17,7 @@ import {
REMOTE_CLUSTER_EDIT_NAME,
} from './remote_clusters_edit.helpers';
import { Cluster } from '../../../common/lib';
import { SECURITY_MODEL } from '../../../common/constants';
let component: TestBed['component'];
let actions: RemoteClustersActions;
@ -64,14 +65,16 @@ describe('Edit Remote cluster', () => {
});
test('should populate the form fields with the values from the remote cluster loaded', () => {
expect(actions.nameInput.getValue()).toBe(REMOTE_CLUSTER_EDIT_NAME);
expect(actions.formStep.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);
expect(actions.formStep.seedsInput.getValue()).toBe(REMOTE_CLUSTER_EDIT.seeds?.join(''));
expect(actions.formStep.skipUnavailableSwitch.isChecked()).toBe(
REMOTE_CLUSTER_EDIT.skipUnavailable
);
});
test('should disable the form name input', () => {
expect(actions.nameInput.isDisabled()).toBe(true);
expect(actions.formStep.nameInput.isDisabled()).toBe(true);
});
describe('on cloud', () => {
@ -83,7 +86,7 @@ describe('Edit Remote cluster', () => {
mode: 'proxy',
proxyAddress: `${cloudUrl}:${defaultCloudPort}`,
serverName: cloudUrl,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
};
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
@ -92,9 +95,11 @@ describe('Edit Remote cluster', () => {
});
component.update();
expect(actions.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.cloudRemoteAddressInput.getValue()).toBe(`${cloudUrl}:${defaultCloudPort}`);
expect(actions.tlsServerNameInput.exists()).toBe(false);
expect(actions.formStep.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.formStep.cloudRemoteAddressInput.getValue()).toBe(
`${cloudUrl}:${defaultCloudPort}`
);
expect(actions.formStep.tlsServerNameInput.exists()).toBe(false);
});
test("existing cluster that doesn't have a TLS server name", async () => {
@ -102,7 +107,7 @@ describe('Edit Remote cluster', () => {
name: REMOTE_CLUSTER_EDIT_NAME,
mode: 'proxy',
proxyAddress: `${cloudUrl}:9500`,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
};
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
@ -111,9 +116,9 @@ describe('Edit Remote cluster', () => {
});
component.update();
expect(actions.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.cloudRemoteAddressInput.getValue()).toBe(`${cloudUrl}:9500`);
expect(actions.tlsServerNameInput.exists()).toBe(true);
expect(actions.formStep.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.formStep.cloudRemoteAddressInput.getValue()).toBe(`${cloudUrl}:9500`);
expect(actions.formStep.tlsServerNameInput.exists()).toBe(true);
});
test('existing cluster that has remote address different from TLS server name)', async () => {
@ -122,7 +127,7 @@ describe('Edit Remote cluster', () => {
mode: 'proxy',
proxyAddress: `${cloudUrl}:${defaultCloudPort}`,
serverName: 'another-value',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
};
httpRequestsMockHelpers.setLoadRemoteClustersResponse([cluster]);
@ -131,9 +136,11 @@ describe('Edit Remote cluster', () => {
});
component.update();
expect(actions.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.cloudRemoteAddressInput.getValue()).toBe(`${cloudUrl}:${defaultCloudPort}`);
expect(actions.tlsServerNameInput.exists()).toBe(true);
expect(actions.formStep.cloudRemoteAddressInput.exists()).toBe(true);
expect(actions.formStep.cloudRemoteAddressInput.getValue()).toBe(
`${cloudUrl}:${defaultCloudPort}`
);
expect(actions.formStep.tlsServerNameInput.exists()).toBe(true);
});
});
});

View file

@ -13,72 +13,108 @@ export interface RemoteClustersActions {
exists: () => boolean;
text: () => string;
};
nameInput: {
setValue: (name: string) => void;
getValue: () => string;
isDisabled: () => boolean;
formStep: {
nameInput: {
setValue: (name: string) => void;
getValue: () => string;
isDisabled: () => boolean;
};
skipUnavailableSwitch: {
exists: () => boolean;
toggle: () => void;
isChecked: () => boolean;
};
connectionModeSwitch: {
exists: () => boolean;
toggle: () => void;
isChecked: () => boolean;
};
cloudAdvancedOptionsSwitch: {
toggle: () => void;
exists: () => boolean;
isChecked: () => boolean;
};
cloudRemoteAddressInput: {
exists: () => boolean;
getValue: () => string;
setValue: (remoteAddress: string) => void;
};
seedsInput: {
setValue: (seed: string) => void;
getValue: () => string;
};
nodeConnectionsInput: {
setValue: (connections: string) => void;
};
proxyAddressInput: {
setValue: (proxyAddress: string) => void;
exists: () => boolean;
};
serverNameInput: {
getLabel: () => string;
exists: () => boolean;
};
tlsServerNameInput: {
getLabel: () => string;
exists: () => boolean;
};
button: {
click: () => void;
isDisabled: () => boolean;
};
backButton: {
click: () => void;
};
isOnFormStep: () => boolean;
};
skipUnavailableSwitch: {
exists: () => boolean;
toggle: () => void;
isChecked: () => boolean;
};
connectionModeSwitch: {
exists: () => boolean;
toggle: () => void;
isChecked: () => boolean;
};
cloudAdvancedOptionsSwitch: {
toggle: () => void;
exists: () => boolean;
isChecked: () => boolean;
};
cloudRemoteAddressInput: {
exists: () => boolean;
getValue: () => string;
setValue: (remoteAddress: string) => void;
};
seedsInput: {
setValue: (seed: string) => void;
getValue: () => string;
};
nodeConnectionsInput: {
setValue: (connections: string) => void;
};
proxyAddressInput: {
setValue: (proxyAddress: string) => void;
exists: () => boolean;
};
serverNameInput: {
getLabel: () => string;
exists: () => boolean;
};
tlsServerNameInput: {
getLabel: () => string;
exists: () => boolean;
};
isOnFirstStep: () => boolean;
saveButton: {
click: () => void;
isDisabled: () => boolean;
};
setupTrust: {
isSubmitInConfirmDisabled: () => boolean;
toggleConfirmSwitch: () => void;
setupTrustConfirmClick: () => void;
backToFirstStepClick: () => void;
setupTrustStep: {
apiCardExist: () => boolean;
certCardExist: () => boolean;
apiCardDocsExist: () => boolean;
certCardDocsExist: () => boolean;
selectApiKeyTrustMode: () => void;
selectCertificatesTrustMode: () => void;
button: {
click: () => void;
isDisabled: () => boolean;
};
isOnTrustStep: () => boolean;
};
reviewStep: {
onPrem: {
exists: () => boolean;
step1LinkExists: () => boolean;
step2LinkExists: () => boolean;
step1Link: () => string;
step2Link: () => string;
};
cloud: {
apiKeyDocumentationExists: () => boolean;
certDocumentationExists: () => boolean;
};
clickAddCluster: () => void;
errorBannerExists: () => boolean;
backButton: {
click: () => void;
};
};
getErrorMessages: () => string[];
globalErrorExists: () => boolean;
}
export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersActions => {
const { form, exists, find, component } = testBed;
const click = (selector: string) => {
act(() => {
find(selector).simulate('click');
});
component.update();
};
const docsButtonExists = () => exists('remoteClusterDocsButton');
const createPageTitleActions = () => {
const pageTitleSelector = 'remoteClusterPageTitle';
return {
@ -88,203 +124,222 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct
},
};
};
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();
const formStepActions = () => {
const createNameInputActions = () => {
const nameInputSelector = 'remoteClusterFormNameInput';
return {
nameInput: {
setValue: (name: string) => form.setInputValue(nameInputSelector, name),
getValue: () => find(nameInputSelector).props().value,
isDisabled: () => find(nameInputSelector).props().disabled,
},
isChecked: () => find(skipUnavailableToggleSelector).props()['aria-checked'],
},
};
};
};
const createConnectionModeActions = () => {
const connectionModeToggleSelector = 'remoteClusterFormConnectionModeToggle';
return {
connectionModeSwitch: {
exists: () => exists(connectionModeToggleSelector),
toggle: () => {
act(() => {
form.toggleEuiSwitch(connectionModeToggleSelector);
});
component.update();
const createSkipUnavailableActions = () => {
const skipUnavailableToggleSelector = 'remoteClusterFormSkipUnavailableFormToggle';
return {
skipUnavailableSwitch: {
exists: () => exists(skipUnavailableToggleSelector),
toggle: () => {
act(() => {
form.toggleEuiSwitch(skipUnavailableToggleSelector);
});
component.update();
},
isChecked: () => find(skipUnavailableToggleSelector).props()['aria-checked'],
},
isChecked: () => find(connectionModeToggleSelector).props()['aria-checked'],
},
};
};
};
const createCloudAdvancedOptionsSwitchActions = () => {
const cloudUrlSelector = 'remoteClusterFormCloudAdvancedOptionsToggle';
return {
cloudAdvancedOptionsSwitch: {
exists: () => exists(cloudUrlSelector),
toggle: () => {
act(() => {
form.toggleEuiSwitch(cloudUrlSelector);
});
component.update();
const createConnectionModeActions = () => {
const connectionModeToggleSelector = 'remoteClusterFormConnectionModeToggle';
return {
connectionModeSwitch: {
exists: () => exists(connectionModeToggleSelector),
toggle: () => {
act(() => {
form.toggleEuiSwitch(connectionModeToggleSelector);
});
component.update();
},
isChecked: () => find(connectionModeToggleSelector).props()['aria-checked'],
},
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 createNodeConnectionsInputActions = () => {
const nodeConnectionsInputSelector = 'remoteClusterFormNodeConnectionsInput';
return {
nodeConnectionsInput: {
setValue: (connections: string) =>
form.setInputValue(nodeConnectionsInputSelector, connections),
},
};
};
const createProxyAddressActions = () => {
const proxyAddressSelector = 'remoteClusterFormProxyAddressInput';
return {
proxyAddressInput: {
setValue: (proxyAddress: string) => form.setInputValue(proxyAddressSelector, proxyAddress),
exists: () => exists(proxyAddressSelector),
},
};
};
const createSetupTrustActions = () => {
const click = () => {
act(() => {
find('remoteClusterFormSaveButton').simulate('click');
});
component.update();
};
const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled;
const setupTrustConfirmClick = () => {
act(() => {
find('setupTrustDoneButton').simulate('click');
});
component.update();
};
};
const backToFirstStepClick = () => {
act(() => {
find('setupTrustBackButton').simulate('click');
});
component.update();
const createCloudAdvancedOptionsSwitchActions = () => {
const cloudUrlSelector = 'remoteClusterFormCloudAdvancedOptionsToggle';
return {
cloudAdvancedOptionsSwitch: {
exists: () => exists(cloudUrlSelector),
toggle: () => {
act(() => {
form.toggleEuiSwitch(cloudUrlSelector);
});
component.update();
},
isChecked: () => find(cloudUrlSelector).props()['aria-checked'],
},
};
};
const isOnFirstStep = () => exists('remoteClusterFormNameInput');
const toggleConfirmSwitch = () => {
act(() => {
const $checkbox = find('remoteClusterTrustCheckbox');
const isChecked = $checkbox.props().checked;
$checkbox.simulate('change', { target: { checked: !isChecked } });
});
component.update();
const createSeedsInputActions = () => {
const seedsInputSelector = 'remoteClusterFormSeedsInput';
return {
seedsInput: {
setValue: (seed: string) => form.setComboBoxValue(seedsInputSelector, seed),
getValue: () => find(seedsInputSelector).text(),
},
};
};
const isSubmitInConfirmDisabled = () => find('remoteClusterTrustSubmitButton').props().disabled;
const createNodeConnectionsInputActions = () => {
const nodeConnectionsInputSelector = 'remoteClusterFormNodeConnectionsInput';
return {
nodeConnectionsInput: {
setValue: (connections: string) =>
form.setInputValue(nodeConnectionsInputSelector, connections),
},
};
};
const apiCardExist = () => exists('setupTrustApiKeyCard');
const certCardExist = () => exists('setupTrustCertCard');
const apiCardDocsExist = () => exists('setupTrustApiKeyCardDocs');
const certCardDocsExist = () => exists('setupTrustCertCardDocs');
const createProxyAddressActions = () => {
const proxyAddressSelector = 'remoteClusterFormProxyAddressInput';
return {
proxyAddressInput: {
setValue: (proxyAddress: string) =>
form.setInputValue(proxyAddressSelector, proxyAddress),
exists: () => exists(proxyAddressSelector),
},
};
};
const formButtonsActions = () => {
const formButtonSelector = 'remoteClusterFormNextButton';
return {
button: {
click: () => click(formButtonSelector),
isDisabled: () => find(formButtonSelector).props().disabled,
},
};
};
const formBackButtonActions = () => {
return {
backButton: {
click: () => click('remoteClusterFormBackButton'),
},
};
};
const isOnFormStepActions = () => {
return { isOnFormStep: () => exists('remoteClusterFormNextButton') };
};
const createServerNameActions = () => {
const serverNameSelector = 'remoteClusterFormServerNameFormRow';
return {
serverNameInput: {
getLabel: () => find('remoteClusterFormServerNameFormRow').find('label').text(),
exists: () => exists(serverNameSelector),
},
};
};
const createTlsServerNameActions = () => {
const serverNameSelector = 'remoteClusterFormTLSServerNameFormRow';
return {
tlsServerNameInput: {
getLabel: () => find(serverNameSelector).find('label').text(),
exists: () => exists(serverNameSelector),
},
};
};
const createCloudRemoteAddressInputActions = () => {
const cloudUrlInputSelector = 'remoteClusterFormRemoteAddressInput';
return {
cloudRemoteAddressInput: {
exists: () => exists(cloudUrlInputSelector),
getValue: () => find(cloudUrlInputSelector).props().value,
setValue: (remoteAddress: string) =>
form.setInputValue(cloudUrlInputSelector, remoteAddress),
},
};
};
return {
isOnFirstStep,
saveButton: { click, isDisabled },
setupTrust: {
setupTrustConfirmClick,
isSubmitInConfirmDisabled,
toggleConfirmSwitch,
apiCardExist,
certCardExist,
apiCardDocsExist,
certCardDocsExist,
backToFirstStepClick,
},
};
};
const createServerNameActions = () => {
const serverNameSelector = 'remoteClusterFormServerNameFormRow';
return {
serverNameInput: {
getLabel: () => find('remoteClusterFormServerNameFormRow').find('label').text(),
exists: () => exists(serverNameSelector),
},
};
};
const createTlsServerNameActions = () => {
const serverNameSelector = 'remoteClusterFormTLSServerNameFormRow';
return {
tlsServerNameInput: {
getLabel: () => find(serverNameSelector).find('label').text(),
exists: () => exists(serverNameSelector),
formStep: {
...createNameInputActions(),
...createSkipUnavailableActions(),
...createConnectionModeActions(),
...createCloudAdvancedOptionsSwitchActions(),
...createSeedsInputActions(),
...createNodeConnectionsInputActions(),
...createProxyAddressActions(),
...formButtonsActions(),
...formBackButtonActions(),
...isOnFormStepActions(),
...createServerNameActions(),
...createTlsServerNameActions(),
...createCloudRemoteAddressInputActions(),
},
};
};
const globalErrorExists = () => exists('remoteClusterFormGlobalError');
const createCloudRemoteAddressInputActions = () => {
const cloudUrlInputSelector = 'remoteClusterFormRemoteAddressInput';
const setupTrustStepActions = () => {
const trustButtonSelector = 'remoteClusterTrustNextButton';
return {
cloudRemoteAddressInput: {
exists: () => exists(cloudUrlInputSelector),
getValue: () => find(cloudUrlInputSelector).props().value,
setValue: (remoteAddress: string) =>
form.setInputValue(cloudUrlInputSelector, remoteAddress),
setupTrustStep: {
apiCardExist: () => exists('setupTrustApiMode'),
certCardExist: () => exists('setupTrustCertMode'),
selectApiKeyTrustMode: () => click('setupTrustApiMode'),
selectCertificatesTrustMode: () => click('setupTrustCertMode'),
button: {
click: () => click(trustButtonSelector),
isDisabled: () => find(trustButtonSelector).props().disabled,
},
isOnTrustStep: () => exists(trustButtonSelector),
},
};
};
const reviewStepActions = () => {
const onPremReviewStepsSelector = 'remoteClusterReviewOnPremSteps';
const onPremStep1Selector = 'remoteClusterReviewOnPremStep1';
const onPremStep2Selector = 'remoteClusterReviewOnPremStep2';
return {
reviewStep: {
onPrem: {
exists: () => exists(onPremReviewStepsSelector),
step1LinkExists: () => exists(onPremStep1Selector),
step2LinkExists: () => exists(onPremStep2Selector),
step1Link: () => find(onPremStep1Selector).props().href,
step2Link: () => find(onPremStep2Selector).props().href,
},
cloud: {
apiKeyDocumentationExists: () => exists('cloudApiKeySteps'),
certDocumentationExists: () => exists('cloudCertDocumentation'),
},
clickAddCluster: () => click('remoteClusterReviewtNextButton'),
errorBannerExists: () => exists('saveErrorBanner'),
backButton: {
click: () => click('remoteClusterReviewtBackButton'),
},
},
};
};
return {
docsButtonExists,
...createPageTitleActions(),
...createNameInputActions(),
...createSkipUnavailableActions(),
...createConnectionModeActions(),
...createCloudAdvancedOptionsSwitchActions(),
...createSeedsInputActions(),
...createNodeConnectionsInputActions(),
...createCloudRemoteAddressInputActions(),
...createProxyAddressActions(),
...createServerNameActions(),
...createTlsServerNameActions(),
...createSetupTrustActions(),
...formStepActions(),
...setupTrustStepActions(),
...reviewStepActions(),
getErrorMessages: form.getErrorsMessages,
globalErrorExists,
};

View file

@ -34,6 +34,7 @@ export const WithAppDependencies =
context={{
isCloudEnabled: !!isCloudEnabled,
cloudBaseUrl: 'test.com',
cloudDeploymentUrl: 'deployment.com',
executionContext: executionContextServiceMock.createStartContract(),
canUseAPIKeyTrustModel: true,
...overrides,

View file

@ -28,13 +28,13 @@ export const SNIFF_MODE = 'sniff';
export const PROXY_MODE = 'proxy';
export const getSecurityModel = (type: string) => {
if (type === 'certificate') {
if (type === SECURITY_MODEL.CERTIFICATE) {
return i18n.translate('xpack.remoteClusters.securityModelCert', {
defaultMessage: 'Certificate',
});
}
if (type === 'api_key') {
if (type === SECURITY_MODEL.API) {
return i18n.translate('xpack.remoteClusters.securityModelApiKey', {
defaultMessage: 'API key',
});
@ -45,3 +45,8 @@ export const getSecurityModel = (type: string) => {
// Hardcoded limit of maximum node connections allowed
export const MAX_NODE_CONNECTIONS = 2 ** 31 - 1; // 2147483647
export enum SECURITY_MODEL {
API = 'api_key',
CERTIFICATE = 'certificate',
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { SECURITY_MODEL } from '../constants';
import { deserializeCluster, serializeCluster } from './cluster_serialization';
describe('cluster_serialization', () => {
@ -40,7 +41,7 @@ describe('cluster_serialization', () => {
skipUnavailable: false,
transportPingSchedule: '-1',
transportCompress: false,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -72,7 +73,7 @@ describe('cluster_serialization', () => {
transportPingSchedule: '-1',
transportCompress: false,
serverName: 'my_server_name',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -94,7 +95,7 @@ describe('cluster_serialization', () => {
maxConnectionsPerCluster: 3,
initialConnectTimeout: '30s',
skipUnavailable: false,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -128,7 +129,7 @@ describe('cluster_serialization', () => {
skipUnavailable: false,
transportPingSchedule: '-1',
transportCompress: false,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -164,7 +165,7 @@ describe('cluster_serialization', () => {
transportPingSchedule: '-1',
transportCompress: false,
serverName: 'localhost',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -186,7 +187,7 @@ describe('cluster_serialization', () => {
connectedNodesCount: 1,
initialConnectTimeout: '30s',
transportCompress: false,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
});
@ -217,7 +218,7 @@ describe('cluster_serialization', () => {
skipUnavailable: false,
transportPingSchedule: '-1',
transportCompress: false,
securityModel: 'api_key',
securityModel: SECURITY_MODEL.API,
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PROXY_MODE } from '../constants';
import { PROXY_MODE, SECURITY_MODEL } from '../constants';
// Values returned from ES GET /_remote/info
/**
@ -113,7 +113,7 @@ export function deserializeCluster(
proxySocketConnections,
connectedSocketsCount,
serverName,
securityModel: clusterCredentials ? 'api_key' : 'certificate',
securityModel: clusterCredentials ? SECURITY_MODEL.API : SECURITY_MODEL.CERTIFICATE,
};
if (transport) {

View file

@ -7,7 +7,7 @@
import { getRandomString } from '@kbn/test-jest-helpers';
import { SNIFF_MODE } from '../common/constants';
import { SECURITY_MODEL, SNIFF_MODE } from '../common/constants';
export const getRemoteClusterMock = ({
name = getRandomString(),
@ -19,7 +19,7 @@ export const getRemoteClusterMock = ({
mode = SNIFF_MODE,
proxyAddress,
hasDeprecatedProxySetting = false,
securityModel = 'certificate',
securityModel = SECURITY_MODEL.CERTIFICATE,
} = {}) => ({
name,
seeds,

View file

@ -1,25 +0,0 @@
// Remote clusters plugin hacks
// Prefix all styles with "remoteClusters" to avoid conflicts.
// Examples
// remoteClustersChart
// remoteClustersChart__legend
// remoteClustersChart__legend--small
// remoteClustersChart__legend-isLoading
/**
* 1. Override EuiFormRow styles. Otherwise the switch will jump around when toggled on and off,
* as the 'Reset to defaults' link is added to and removed from the DOM.
* 2. Fix the positioning.
*/
.remoteClusterSkipIfUnavailableSwitch {
justify-content: flex-start !important; /* 1 */
padding-top: $euiSizeS !important;
}
/**
* 1. Prevent inherited flexbox layout from compressing this element on IE.
*/
.remoteClustersConnectionStatus__message {
flex-basis: auto !important; /* 1 */
}

View file

@ -11,6 +11,7 @@ import { ExecutionContextStart } from '@kbn/core/public';
export interface Context {
isCloudEnabled: boolean;
cloudBaseUrl: string;
cloudDeploymentUrl: string;
executionContext: ExecutionContextStart;
canUseAPIKeyTrustModel: boolean;
}

View file

@ -13,6 +13,7 @@ export declare const renderApp: (
appDependencies: {
isCloudEnabled: boolean;
cloudBaseUrl: string;
cloudDeploymentUrl: string;
executionContext: ExecutionContextStart;
canUseAPIKeyTrustModel: boolean;
},

View file

@ -14,8 +14,6 @@ import { App } from './app';
import { remoteClustersStore } from './store';
import { AppContextProvider } from './app_context';
import './_hacks.scss';
const AppWithExecutionContext = ({ history, executionContext }) => {
useExecutionContext(executionContext, {
type: 'application',

View file

@ -4,14 +4,11 @@
* 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 { EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
export function ConfiguredByNodeWarning() {
export const ConfiguredByNodeWarning: React.FC = () => {
return (
<EuiCallOut
title={
@ -26,4 +23,4 @@ export function ConfiguredByNodeWarning() {
data-test-subj="remoteClusterConfiguredByNodeWarning"
/>
);
}
};

View file

@ -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, { ReactNode, useCallback, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
} from '../../../../../shared_imports';
import { ClusterPayload } from '../../../../../../common/lib';
import { RequestFlyout } from './request_flyout';
interface Props {
showRequest: boolean;
disabled?: boolean;
isSaving?: boolean;
handleNext: () => void;
onBack?: () => void;
confirmFormText: ReactNode;
backFormText?: ReactNode;
cluster?: ClusterPayload;
nextButtonTestSubj: string;
backButtonTestSubj?: string;
}
export const ActionButtons: React.FC<Props> = ({
showRequest,
handleNext,
disabled,
isSaving,
onBack,
confirmFormText,
backFormText,
cluster,
nextButtonTestSubj,
backButtonTestSubj,
}) => {
const [isRequestVisible, setIsRequestVisible] = useState(false);
const toggleRequest = useCallback(() => {
setIsRequestVisible((prev) => !prev);
}, []);
return (
<EuiFlexGroup wrap justifyContent="center">
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
color="primary"
fill
onClick={handleNext}
disabled={disabled}
isLoading={isSaving}
data-test-subj={nextButtonTestSubj}
>
{confirmFormText}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{onBack && (
<EuiButtonEmpty color="primary" onClick={onBack} data-test-subj={backButtonTestSubj}>
{backFormText}
</EuiButtonEmpty>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
{showRequest && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={toggleRequest} data-test-subj="remoteClustersRequestButton">
{isRequestVisible ? (
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel"
defaultMessage="Hide request"
/>
) : (
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.showRequestButtonLabel"
defaultMessage="Show request"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlexItem>
{isRequestVisible && cluster ? (
<RequestFlyout cluster={cluster} close={() => setIsRequestVisible(false)} />
) : null}
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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 { ActionButtons } from './action_buttons';
export { RequestFlyout } from './request_flyout';
export { SaveError } from './save_error';

View file

@ -0,0 +1,37 @@
/*
* 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 { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { RequestError } from '../../../../../types';
interface Props {
saveError: RequestError;
}
export const SaveError: React.FC<Props> = ({ saveError }) => {
const { message, cause } = saveError;
const renderErrorBody = () => {
if (!cause || !Array.isArray(cause)) return null;
return cause.length === 1 ? (
<p>{cause[0]}</p>
) : (
<ul>
{cause.map((causeValue, index) => (
<li key={index}>{causeValue}</li>
))}
</ul>
);
};
return (
<>
<EuiCallOut title={message} color="danger" iconType="error" data-test-subj="saveErrorBanner">
{renderErrorBody()}
</EuiCallOut>
<EuiSpacer />
</>
);
};

View file

@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiDescribedFormGroup, EuiTitle, EuiFormRow, EuiSwitch } from '@elastic/eui';
import { EuiDescribedFormGroup, EuiTitle, EuiFormRow, EuiSwitch, EuiSpacer } from '@elastic/eui';
import { SNIFF_MODE, PROXY_MODE } from '../../../../../../../common/constants';
import { useAppContext } from '../../../../../app_context';
@ -52,12 +52,13 @@ export const ConnectionMode: FunctionComponent<Props> = (props) => {
id="xpack.remoteClusters.remoteClusterForm.sectionModeDescription"
defaultMessage="Use seed nodes by default, or switch to proxy mode."
/>
<EuiSpacer size="l" />
<EuiFormRow fullWidth>
<EuiSwitch
label={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.fieldModeLabel"
defaultMessage="Use proxy mode"
defaultMessage="Manually enter proxy address and server name"
/>
}
checked={mode === PROXY_MODE}

View file

@ -18,6 +18,7 @@ import {
EuiLink,
EuiFieldNumber,
EuiCode,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -53,10 +54,13 @@ export const ConnectionModeCloud: FunctionComponent<Props> = (props) => {
}
description={
<>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription"
defaultMessage="Configure how to connect to the remote cluster."
/>
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription"
defaultMessage="Configure how to connect to the remote cluster."
/>
</EuiText>
<EuiSpacer size="l" />
<EuiFormRow fullWidth>
<EuiSwitch
label={
@ -99,9 +103,9 @@ export const ConnectionModeCloud: FunctionComponent<Props> = (props) => {
<EuiFieldText
value={cloudRemoteAddress}
placeholder={i18n.translate(
'xpack.remoteClusters.remoteClusterForm.fieldProxyAddressPlaceholder',
'xpack.remoteClusters.remoteClusterForm.fieldRemoteAddressPlaceholder',
{
defaultMessage: 'host:port',
defaultMessage: 'hostname:port',
}
)}
onChange={(e) => onFieldsChange({ cloudRemoteAddress: e.target.value })}

View file

@ -5,23 +5,18 @@
* 2.0.
*/
import React, { Component, Fragment } from 'react';
import React, { useState, useContext, useCallback, useEffect } from 'react';
import { merge } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiDescribedFormGroup,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiLink,
EuiLoadingLogo,
EuiOverlayMask,
EuiSpacer,
EuiSwitch,
EuiTitle,
@ -29,16 +24,14 @@ import {
EuiScreenReaderOnly,
htmlIdGenerator,
EuiSwitchEvent,
useEuiTheme,
EuiText,
} from '@elastic/eui';
import { ReactNode } from 'react-markdown';
import { Cluster, ClusterPayload } from '../../../../../../common/lib';
import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants';
import { AppContext, Context } from '../../../../app_context';
import { AppContext } from '../../../../app_context';
import { skippingDisconnectedClustersUrl } from '../../../../services/documentation';
import { RequestFlyout } from '../components/request_flyout';
import { ConnectionMode } from './components';
import {
ClusterErrors,
@ -46,7 +39,7 @@ import {
validateCluster,
isCloudAdvancedOptionsEnabled,
} from './validators';
import { ActionButtons, SaveError } from '../components';
const defaultClusterValues: ClusterPayload = {
name: '',
seeds: [],
@ -56,148 +49,76 @@ const defaultClusterValues: ClusterPayload = {
proxySocketConnections: 18,
serverName: '',
};
const ERROR_TITLE_ID = 'removeClustersErrorTitle';
const ERROR_LIST_ID = 'removeClustersErrorList';
interface Props {
save: (cluster: ClusterPayload) => void;
cancel?: () => void;
confirmFormAction: (cluster: ClusterPayload) => void;
onBack?: () => void;
isSaving?: boolean;
saveError?: any;
cluster?: Cluster;
onConfigChange?: (cluster: ClusterPayload, hasErrors: boolean) => void;
confirmFormText: ReactNode;
backFormText: ReactNode;
}
export type FormFields = ClusterPayload & {
cloudRemoteAddress?: string;
cloudAdvancedOptionsEnabled: 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;
declare context: Context;
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,
cloudRemoteAddress: cluster?.proxyAddress || '',
cloudAdvancedOptionsEnabled: isCloudAdvancedOptionsEnabled(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 cloud remote address changes, fill proxy address and server name
const { cloudRemoteAddress, cloudAdvancedOptionsEnabled } = changedFields;
if (cloudRemoteAddress) {
const { proxyAddress, serverName } =
convertCloudRemoteAddressToProxyConnection(cloudRemoteAddress);
// Only change the server name if the advanced options are not currently open
if (this.state.fields.cloudAdvancedOptionsEnabled) {
changedFields = {
...changedFields,
proxyAddress,
};
} else {
changedFields = {
...changedFields,
proxyAddress,
serverName,
};
}
}
// If we switch off the advanced options, revert the server name to
// the host name from the proxy address
if (cloudAdvancedOptionsEnabled === false) {
changedFields = {
...changedFields,
serverName: this.state.fields.proxyAddress?.split(':')[0],
proxySocketConnections: defaultClusterValues.proxySocketConnections,
};
}
this.setState(({ fields: prevFields }) => {
const newFields = {
...prevFields,
...changedFields,
};
return {
fields: newFields,
fieldsErrors: validateCluster(newFields, isCloudEnabled),
};
});
};
getCluster(): ClusterPayload {
export const RemoteClusterForm: React.FC<Props> = ({
confirmFormAction,
onBack,
isSaving,
saveError,
cluster,
onConfigChange,
confirmFormText,
backFormText,
}) => {
const context = useContext(AppContext);
const { euiTheme } = useEuiTheme();
const { isCloudEnabled } = context;
const defaultMode = isCloudEnabled ? PROXY_MODE : SNIFF_MODE;
const initialFieldsState: FormFields = merge(
{},
{
...defaultClusterValues,
mode: defaultMode,
cloudRemoteAddress: cluster?.proxyAddress || '',
cloudAdvancedOptionsEnabled: isCloudAdvancedOptionsEnabled(cluster),
},
cluster
);
const [fields, setFields] = useState<FormFields>(initialFieldsState);
const [fieldsErrors, setFieldsErrors] = useState<ClusterErrors>(
validateCluster(initialFieldsState, isCloudEnabled)
);
const [areErrorsVisible, setAreErrorsVisible] = useState(false);
const [formHasBeenSubmited, setFormHasBeenSubmited] = useState(false);
const generateId = htmlIdGenerator();
const getCluster = useCallback((): ClusterPayload => {
const {
fields: {
name,
mode,
seeds,
nodeConnections,
proxyAddress,
proxySocketConnections,
serverName,
skipUnavailable,
},
} = this.state;
const { cluster } = this.props;
name,
mode,
seeds,
nodeConnections,
proxyAddress,
proxySocketConnections,
serverName,
skipUnavailable,
} = fields;
let modeSettings;
if (mode === PROXY_MODE) {
modeSettings = {
proxyAddress,
proxySocketConnections,
serverName,
};
} else {
modeSettings = {
seeds,
nodeConnections,
};
}
const modeSettings =
mode === PROXY_MODE
? {
proxyAddress,
proxySocketConnections,
serverName,
}
: {
seeds,
nodeConnections,
};
return {
name,
@ -206,44 +127,88 @@ export class RemoteClusterForm extends Component<Props, State> {
hasDeprecatedProxySetting: cluster?.hasDeprecatedProxySetting,
...modeSettings,
};
}
}, [fields, cluster]);
save = () => {
const { save } = this.props;
if (this.hasErrors()) {
this.setState({
areErrorsVisible: true,
});
const handleNext = () => {
if (hasErrors()) {
setAreErrorsVisible(true);
return;
}
const cluster = this.getCluster();
save(cluster);
setFormHasBeenSubmited(true);
confirmFormAction(getCluster());
};
onSkipUnavailableChange = (e: EuiSwitchEvent) => {
const skipUnavailable = e.target.checked;
this.onFieldsChange({ skipUnavailable });
};
const onFieldsChange = useCallback(
(changedFields: Partial<FormFields>) => {
// when cloud remote address changes, fill proxy address and server name
const { cloudRemoteAddress, cloudAdvancedOptionsEnabled } = changedFields;
if (cloudRemoteAddress) {
const { proxyAddress, serverName } =
convertCloudRemoteAddressToProxyConnection(cloudRemoteAddress);
// Only change the server name if the advanced options are not currently open
if (fields.cloudAdvancedOptionsEnabled) {
changedFields = {
...changedFields,
proxyAddress,
};
} else {
changedFields = {
...changedFields,
proxyAddress,
serverName,
};
}
}
// If we switch off the advanced options, revert the server name to
// the host name from the proxy address
if (cloudAdvancedOptionsEnabled === false) {
changedFields = {
...changedFields,
serverName: fields.proxyAddress?.split(':')[0],
proxySocketConnections: defaultClusterValues.proxySocketConnections,
};
}
const newFields = {
...fields,
...changedFields,
};
setFields(newFields);
setFieldsErrors(validateCluster(newFields, isCloudEnabled));
},
[fields, isCloudEnabled]
);
resetToDefault = (fieldName: keyof ClusterPayload) => {
this.onFieldsChange({
[fieldName]: defaultClusterValues[fieldName],
});
};
hasErrors = () => {
const { fieldsErrors } = this.state;
const hasErrors = useCallback(() => {
const errorValues = Object.values(fieldsErrors);
return errorValues.some((error) => error != null);
};
}, [fieldsErrors]);
renderSkipUnavailable() {
const {
fields: { skipUnavailable },
} = this.state;
useEffect(() => {
if (onConfigChange && formHasBeenSubmited) {
const errors = hasErrors();
setAreErrorsVisible(errors);
onConfigChange(getCluster(), errors);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fields]);
const onSkipUnavailableChange = useCallback(
(e: EuiSwitchEvent) => {
const skipUnavailable = e.target.checked;
onFieldsChange({ skipUnavailable });
},
[onFieldsChange]
);
const resetToDefault = useCallback(
(fieldName: keyof ClusterPayload) => {
onFieldsChange({
[fieldName]: defaultClusterValues[fieldName],
});
},
[onFieldsChange]
);
const renderSkipUnavailable = () => {
return (
<EuiDescribedFormGroup
title={
@ -257,44 +222,36 @@ export class RemoteClusterForm extends Component<Props, State> {
</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>
<EuiText size="s">
<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 Skip if unavailable. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink href={skippingDisconnectedClustersUrl} target="_blank">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel"
defaultMessage="Learn more."
/>
</EuiLink>
),
}}
/>
</EuiText>
}
fullWidth
>
<EuiFormRow
data-test-subj="remoteClusterFormSkipUnavailableFormRow"
className="remoteClusterSkipIfUnavailableSwitch"
css={css`
padding-top: ${euiTheme.size.s};
`}
fullWidth
helpText={
skipUnavailable !== defaultClusterValues.skipUnavailable ? (
fields.skipUnavailable !== defaultClusterValues.skipUnavailable ? (
<EuiLink
onClick={() => {
this.resetToDefault('skipUnavailable');
resetToDefault('skipUnavailable');
}}
>
<FormattedMessage
@ -312,143 +269,25 @@ export class RemoteClusterForm extends Component<Props, State> {
defaultMessage: 'Skip if unavailable',
}
)}
checked={!!skipUnavailable}
onChange={this.onSkipUnavailableChange}
checked={!!fields.skipUnavailable}
onChange={onSkipUnavailableChange}
data-test-subj="remoteClusterFormSkipUnavailableFormToggle"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
}
};
renderActions() {
const { isSaving, cancel, cluster: isEditMode } = this.props;
const { areErrorsVisible, isRequestVisible } = this.state;
const isSaveDisabled = (areErrorsVisible && this.hasErrors()) || isSaving;
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
{cancel && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="primary" onClick={cancel}>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={this.toggleRequest}
data-test-subj="remoteClustersRequestButton"
>
{isRequestVisible ? (
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.hideRequestButtonLabel"
defaultMessage="Hide request"
/>
) : (
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.showRequestButtonLabel"
defaultMessage="Show request"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="remoteClusterFormSaveButton"
color="primary"
onClick={this.save}
fill
isDisabled={isSaveDisabled}
isLoading={isSaving}
aria-describedby={`${this.generateId(ERROR_TITLE_ID)} ${this.generateId(
ERROR_LIST_ID
)}`}
>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.nextButtonLabel"
defaultMessage="{isEditMode, select, true{Save} other{Next}}"
values={{
isEditMode: Boolean(isEditMode),
}}
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
renderSavingFeedback() {
if (this.props.isSaving) {
return (
<EuiOverlayMask>
<EuiLoadingLogo logo="logoKibana" 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} color="danger" iconType="error">
{errorBody}
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
}
return null;
}
renderErrors = () => {
const renderErrors = () => {
const {
areErrorsVisible,
fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress },
} = this.state;
const hasErrors = this.hasErrors();
if (!areErrorsVisible || !hasErrors) {
name: errorClusterName,
seeds: errorsSeeds,
proxyAddress: errorProxyAddress,
} = fieldsErrors;
if (!areErrorsVisible || !hasErrors()) {
return null;
}
const errorExplanations = [];
if (errorClusterName) {
errorExplanations.push({
key: 'nameExplanation',
@ -458,7 +297,6 @@ export class RemoteClusterForm extends Component<Props, State> {
error: errorClusterName,
});
}
if (errorsSeeds) {
errorExplanations.push({
key: 'seedsExplanation',
@ -468,7 +306,6 @@ export class RemoteClusterForm extends Component<Props, State> {
error: errorsSeeds,
});
}
if (errorProxyAddress) {
errorExplanations.push({
key: 'proxyAddressExplanation',
@ -478,10 +315,9 @@ export class RemoteClusterForm extends Component<Props, State> {
error: errorProxyAddress,
});
}
const messagesToBeRendered = errorExplanations.length && (
<EuiScreenReaderOnly>
<dl id={this.generateId(ERROR_LIST_ID)} aria-labelledby={this.generateId(ERROR_TITLE_ID)}>
<dl id={generateId(ERROR_LIST_ID)} aria-labelledby={generateId(ERROR_TITLE_ID)}>
{errorExplanations.map(({ key, field, error }) => (
<div key={key}>
<dt>{field}</dt>
@ -491,12 +327,11 @@ export class RemoteClusterForm extends Component<Props, State> {
</dl>
</EuiScreenReaderOnly>
);
return (
<Fragment>
<>
<EuiCallOut
title={
<span id={this.generateId(ERROR_TITLE_ID)}>
<span id={generateId(ERROR_TITLE_ID)}>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
defaultMessage="Some fields require your attention."
@ -508,92 +343,102 @@ export class RemoteClusterForm extends Component<Props, State> {
/>
<EuiDelayRender>{messagesToBeRendered}</EuiDelayRender>
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
</Fragment>
</>
);
};
render() {
const { isRequestVisible, areErrorsVisible, fields, fieldsErrors } = this.state;
const { name: errorClusterName } = fieldsErrors;
const { cluster } = this.props;
const isNew = !cluster;
return (
<Fragment>
{this.renderSaveErrorFeedback()}
{this.renderErrors()}
<EuiForm data-test-subj="remoteClusterForm">
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
defaultMessage="Name"
/>
</h2>
</EuiTitle>
}
description={
const isNew = !cluster;
return (
<>
{saveError && <SaveError saveError={saveError} />}
{renderErrors()}
<EuiForm data-test-subj="remoteClusterForm">
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h2>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
defaultMessage="Remote cluster name"
/>
</h2>
</EuiTitle>
}
description={
isCloudEnabled ? (
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.sectionNameDescription"
defaultMessage="A unique identifier for the remote cluster. Must match the {remoteClusterName} in this deployments Cloud -> Security settings."
values={{
remoteClusterName: (
<strong>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.sectionNameDescription.remoteClusterName"
defaultMessage="remote cluster name"
/>
</strong>
),
}}
/>
</EuiText>
) : (
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionNameDescription"
defaultMessage="A unique name for the cluster."
id="xpack.remoteClusters.remoteClusterForm.stateful.sectionNameDescription"
defaultMessage="A unique identifier for the remote cluster."
/>
)
}
fullWidth
>
<EuiFormRow
data-test-subj="remoteClusterFormNameFormRow"
label={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabel"
defaultMessage="Remote cluster name"
/>
}
helpText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.fieldNameLabelHelpText"
defaultMessage="Must contain only letters, numbers, underscores, and dashes."
/>
}
error={fieldsErrors.name}
isInvalid={Boolean(areErrorsVisible && fieldsErrors.name)}
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)}
<EuiFieldText
isInvalid={Boolean(areErrorsVisible && fieldsErrors.name)}
value={fields.name}
onChange={(e) => onFieldsChange({ name: e.target.value })}
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>
<EuiSpacer size="l" />
{this.renderActions()}
{this.renderSavingFeedback()}
{isRequestVisible ? (
<RequestFlyout
cluster={this.getCluster()}
close={() => this.setState({ isRequestVisible: false })}
/>
) : null}
</Fragment>
);
}
}
disabled={!isNew}
data-test-subj="remoteClusterFormNameInput"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<ConnectionMode
fields={fields}
fieldsErrors={fieldsErrors}
onFieldsChange={onFieldsChange}
areErrorsVisible={areErrorsVisible}
/>
{renderSkipUnavailable()}
</EuiForm>
<EuiSpacer size="xl" />
<ActionButtons
showRequest={true}
disabled={areErrorsVisible && hasErrors()}
isSaving={isSaving}
handleNext={handleNext}
onBack={onBack}
confirmFormText={confirmFormText}
backFormText={backFormText}
cluster={getCluster()}
nextButtonTestSubj={'remoteClusterFormNextButton'}
backButtonTestSubj={'remoteClusterFormBackButton'}
/>
</>
);
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { SECURITY_MODEL } from '../../../../../../../common/constants';
import {
isCloudAdvancedOptionsEnabled,
validateCloudRemoteAddress,
@ -35,7 +36,7 @@ describe('Cloud remote address', () => {
const actual = isCloudAdvancedOptionsEnabled({
name: 'test',
proxyAddress: '',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(actual).toBe(false);
});
@ -45,7 +46,7 @@ describe('Cloud remote address', () => {
name: 'test',
proxyAddress: 'some-proxy:9400',
serverName: 'some-proxy',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(actual).toBe(false);
});
@ -54,7 +55,7 @@ describe('Cloud remote address', () => {
name: 'test',
proxyAddress: 'some-proxy:9400',
serverName: 'some-server-name',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(actual).toBe(true);
});
@ -64,7 +65,7 @@ describe('Cloud remote address', () => {
proxyAddress: 'some-proxy:9400',
serverName: 'some-proxy-name',
proxySocketConnections: 19,
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(actual).toBe(true);
});

View file

@ -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 { RemoteClusterReview } from './remote_cluster_review';

View file

@ -0,0 +1,329 @@
/*
* 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, { useContext } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiText, EuiLink, EuiSteps, EuiTitle, EuiStepsProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RequestError } from '../../../../../types';
import { SECURITY_MODEL } from '../../../../../../common/constants';
import {
apiKeys,
cloudCreateApiKey,
cloudSetupTrustUrl,
onPremPrerequisitesApiKey,
onPremSecurityApiKey,
onPremPrerequisitesCert,
onPremSecurityCert,
} from '../../../../services/documentation';
import { ClusterPayload } from '../../../../../../common/lib';
import { AppContext } from '../../../../app_context';
import { ActionButtons, SaveError } from '../components';
interface Props {
onBack?: () => void;
onSubmit: () => void;
isSaving?: boolean;
saveError?: RequestError;
cluster: ClusterPayload;
securityModel: string;
}
export const RemoteClusterReview = ({
onBack,
onSubmit,
isSaving,
saveError,
cluster,
securityModel,
}: Props) => {
const context = useContext(AppContext);
const { isCloudEnabled, cloudDeploymentUrl } = context;
const onPremSteps: EuiStepsProps['steps'] = [
{
title: i18n.translate('xpack.remoteClusters.remoteClusterForm.onPrem.step1.title', {
defaultMessage: 'Confirm both clusters are compatible',
}),
status: 'incomplete',
children: (
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.onPrem.step1.paragraph"
defaultMessage="Ensure that both clusters meet the {requirmentsLink} needed to enable the connection."
values={{
requirmentsLink: (
<EuiLink
href={
securityModel === SECURITY_MODEL.API
? onPremPrerequisitesApiKey
: onPremPrerequisitesCert
}
external={true}
target="_blank"
data-test-subj="remoteClusterReviewOnPremStep1"
>
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.onPrem.step1.requirements',
{
defaultMessage: 'requirements',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
),
},
{
title: i18n.translate('xpack.remoteClusters.remoteClusterForm.onPrem.step2.title', {
defaultMessage: 'Confirm trust is established',
}),
status: 'incomplete',
children: (
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.onPrem.step2.paragraph"
defaultMessage="Follow the {addRemoteClusterGuideLink} to establish trust between local and remote clusters."
values={{
addRemoteClusterGuideLink: (
<EuiLink
href={
securityModel === SECURITY_MODEL.API ? onPremSecurityApiKey : onPremSecurityCert
}
external={true}
target="_blank"
data-test-subj="remoteClusterReviewOnPremStep2"
>
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.onPrem.step1.addClustersGuide',
{
defaultMessage: 'Add remote clusters guide',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
),
},
];
const cloudSteps: EuiStepsProps['steps'] = [
{
title: i18n.translate('xpack.remoteClusters.remoteClusterForm.cloud.api.step1.title', {
defaultMessage: 'Create a cross-cluster API key on the remote deployment',
}),
status: 'incomplete',
'data-test-subj': 'cloudApiKeySteps',
children: (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step1.paragraph1"
defaultMessage="On the remote cluster or deployment, use the {apiLink} or {kibanaLink} to create a cross-cluster API key. Configure it with access to the indices you want to use for cross-cluster search or cross-cluster replication."
values={{
apiLink: (
<EuiLink href={cloudCreateApiKey} external={true} target="_blank">
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.cloud.api.step1.paragraph1.api',
{
defaultMessage: 'Elasticsearch API',
}
)}
</EuiLink>
),
kibanaLink: (
<EuiLink href={apiKeys} external={true} target="_blank">
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.cloud.api.step1.paragraph1.kibana',
{
defaultMessage: 'Kibana',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="l" />
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step1.paragraph2"
defaultMessage="Copy the encoded key (the “encoded” value from the response) to a safe location. You will need it in the next step."
/>
</EuiText>
</>
),
},
{
title: i18n.translate('xpack.remoteClusters.remoteClusterForm.cloud.api.step2.title', {
defaultMessage: 'Configure your local deployment',
}),
status: 'incomplete',
children: (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step2.intro"
defaultMessage="Add the key that you created to your local deployment's keystore:"
/>
</EuiText>
<EuiText size="s">
<ol>
<li>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step2.list1"
defaultMessage="Go to the {managementLink} page for your deployment."
values={{
managementLink: (
<EuiLink href={cloudDeploymentUrl} target="_blank">
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.cloud.api.step2.list1.management',
{
defaultMessage: 'management',
}
)}
</EuiLink>
),
}}
/>
</li>
<li>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step2.list2"
defaultMessage="From the navigation menu, click Security."
/>
</li>
<li>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step2.list3"
defaultMessage="In the Remote connections section, add your API key."
/>
</li>
</ol>
</EuiText>
<EuiSpacer size="l" />
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.api.step2.end"
defaultMessage="Check {documentationLink} for more detailed instructions."
values={{
documentationLink: (
<EuiLink href={cloudSetupTrustUrl} external={true} target="_blank">
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.cloud.api.step2.end.documentation',
{
defaultMessage: 'documentation',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
</>
),
},
];
const getOnPremInfo = () => {
return (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.onPrem.disclaimerInfo"
defaultMessage="Check that the following requirements are completed to ensure that both clusters can communicate:"
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSteps
data-test-subj="remoteClusterReviewOnPremSteps"
titleSize="s"
steps={onPremSteps}
/>
</>
);
};
const getCloudInfo = () => {
return securityModel === SECURITY_MODEL.API ? (
<>
<EuiText size="s">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.disclaimerInfo"
defaultMessage="Make sure you complete the steps below before proceeding with saving this configuration."
/>
</EuiText>
<EuiSpacer size="l" />
<EuiSteps titleSize="s" steps={cloudSteps} />
</>
) : (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.cert.title"
defaultMessage="Confirm trust is established."
/>
</h3>
</EuiTitle>
<EuiSpacer size="l" />
<EuiText size="s" data-test-subj="cloudCertDocumentation">
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloud.cert.paragraph"
defaultMessage="Before you proceed, ensure that trust is correctly configured between both clusters. If all requirements are not met, the remote cluster won't connect. {detailsLink}"
values={{
detailsLink: (
<EuiLink href={cloudSetupTrustUrl} external={true} target="_blank">
{i18n.translate(
'xpack.remoteClusters.remoteClusterForm.cloud.cert.paragraph.documentationLink',
{
defaultMessage: 'Read details.',
}
)}
</EuiLink>
),
}}
data-test-subj="cloudCertDocumentation"
/>
</EuiText>
</>
);
};
return (
<>
{saveError && <SaveError saveError={saveError} />}
{isCloudEnabled ? getCloudInfo() : getOnPremInfo()}
<EuiSpacer size="xl" />
<ActionButtons
showRequest={true}
isSaving={isSaving}
handleNext={onSubmit}
onBack={onBack}
confirmFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.addClusterButtonLabel"
defaultMessage="Add remote cluster"
/>
}
backFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.backButtonLabel"
defaultMessage="Back"
/>
}
cluster={cluster}
nextButtonTestSubj={'remoteClusterReviewtNextButton'}
backButtonTestSubj={'remoteClusterReviewtBackButton'}
/>
</>
);
};

View file

@ -1,107 +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, { useState, FormEvent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiText,
EuiSpacer,
EuiButtonEmpty,
EuiForm,
EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiCheckbox,
useGeneratedHtmlId,
} from '@elastic/eui';
interface ModalProps {
closeModal: () => void;
onSubmit: () => void;
}
export const ConfirmTrustSetupModal = ({ closeModal, onSubmit }: ModalProps) => {
const [hasSetupTrust, setHasSetupTrust] = useState<boolean>(false);
const modalFormId = useGeneratedHtmlId({ prefix: 'modalForm' });
const checkBoxId = useGeneratedHtmlId({ prefix: 'checkBoxId' });
const onFormSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
closeModal();
onSubmit();
};
return (
<EuiModal onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.modal.title"
defaultMessage="Confirm your configuration"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
<p>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.body"
defaultMessage="Have you set up trust to connect to your remote cluster?"
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiForm id={modalFormId} component="form" onSubmit={onFormSubmit}>
<EuiFormRow>
<EuiCheckbox
id={checkBoxId}
label={i18n.translate('xpack.remoteClusters.clusterWizard.trustStep.modal.checkbox', {
defaultMessage: 'Yes, I have setup trust',
})}
labelProps={{
'data-test-subj': 'remoteClusterTrustCheckboxLabel',
}}
checked={hasSetupTrust}
onChange={() => setHasSetupTrust(!hasSetupTrust)}
data-test-subj="remoteClusterTrustCheckbox"
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={closeModal}>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.modal.cancelButton"
defaultMessage="No, go back"
/>
</EuiButtonEmpty>
<EuiButton
fill
type="submit"
form={modalFormId}
disabled={!hasSetupTrust}
data-test-subj="remoteClusterTrustSubmitButton"
>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.modal.createCluster"
defaultMessage="Add remote cluster"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -5,22 +5,20 @@
* 2.0.
*/
import React, { useState, useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { SECURITY_MODEL } from '../../../../../../common/constants';
import {
EuiSpacer,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
} from '../../../../../shared_imports';
import * as docs from '../../../../services/documentation';
import { AppContext } from '../../../../app_context';
import { ConfirmTrustSetupModal } from './confirm_modal';
import { ActionButtons } from '../components';
const MIN_ALLOWED_VERSION_API_KEYS_METHOD = '8.10';
const CARD_MAX_WIDTH = 400;
@ -48,21 +46,23 @@ const i18nTexts = {
),
};
const docLinks = {
cert: docs.onPremSetupTrustWithCertUrl,
apiKey: docs.onPremSetupTrustWithApiKeyUrl,
cloud: docs.cloudSetupTrustUrl,
};
interface Props {
onBack: () => void;
onSubmit: () => void;
isSaving: boolean;
next: (model: string) => void;
onSecurityChange: (model: string) => void;
currentSecurityModel: string;
}
export const RemoteClusterSetupTrust = ({ onBack, onSubmit, isSaving }: Props) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const { canUseAPIKeyTrustModel, isCloudEnabled } = useContext(AppContext);
export const RemoteClusterSetupTrust = ({
next,
currentSecurityModel,
onSecurityChange,
}: Props) => {
const { canUseAPIKeyTrustModel } = useContext(AppContext);
const [securityModel, setSecurityModel] = useState<string>(currentSecurityModel);
useEffect(() => {
onSecurityChange(securityModel);
}, [onSecurityChange, securityModel]);
return (
<div>
@ -70,123 +70,72 @@ export const RemoteClusterSetupTrust = ({ onBack, onSubmit, isSaving }: Props) =
<p>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.title"
defaultMessage="Set up an authentication mechanism to connect to the remote cluster. Complete{br} this step using the instructions in our docs before continuing."
values={{
br: <br />,
}}
defaultMessage="Set up an authentication mechanism to connect to the remote cluster."
/>
</p>
</EuiText>
<EuiSpacer size="xxl" />
<EuiFlexGroup wrap justifyContent="center">
<EuiFlexGroup gutterSize="l" wrap justifyContent="center">
{canUseAPIKeyTrustModel && (
<EuiFlexItem style={{ maxWidth: CARD_MAX_WIDTH }}>
<EuiCard
title={i18nTexts.apiKeyTitle}
paddingSize="l"
data-test-subj="setupTrustApiKeyCard"
>
<EuiText size="s">
<p>{i18nTexts.apiKeyDescription}</p>
</EuiText>
<EuiSpacer size="xl" />
<EuiButton
href={isCloudEnabled ? docLinks.cloud : docLinks.apiKey}
target="_blank"
data-test-subj="setupTrustApiKeyCardDocs"
>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.docs"
defaultMessage="View instructions"
/>
</EuiButton>
<EuiSpacer size="xl" />
<EuiText size="xs" color="subdued">
<p>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.apiKeyNote"
defaultMessage="Both clusters must be on version {minAllowedVersion} or above."
values={{ minAllowedVersion: MIN_ALLOWED_VERSION_API_KEYS_METHOD }}
/>
</p>
</EuiText>
</EuiCard>
data-test-subj="setupTrustApiMode"
title={i18nTexts.apiKeyTitle}
description={i18nTexts.apiKeyDescription}
footer={
<EuiText size="xs" color="subdued">
<p>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.apiKeyNote"
defaultMessage="Both clusters must be on version {minAllowedVersion} or above."
values={{ minAllowedVersion: MIN_ALLOWED_VERSION_API_KEYS_METHOD }}
/>
</p>
</EuiText>
}
selectable={{
onClick: () => {
setSecurityModel(SECURITY_MODEL.API);
},
isSelected: securityModel === SECURITY_MODEL.API,
}}
/>
</EuiFlexItem>
)}
<EuiFlexItem style={{ maxWidth: CARD_MAX_WIDTH }}>
<EuiCard
title={
<>
<EuiSpacer size="s" />
{i18nTexts.certTitle}
</>
}
paddingSize="l"
data-test-subj="setupTrustCertCard"
>
<EuiText size="s">
<p>{i18nTexts.certDescription}</p>
</EuiText>
<EuiSpacer size="xl" />
<EuiButton
href={isCloudEnabled ? docLinks.cloud : docLinks.cert}
target="_blank"
data-test-subj="setupTrustCertCardDocs"
>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.docs"
defaultMessage="View instructions"
/>
</EuiButton>
</EuiCard>
data-test-subj="setupTrustCertMode"
title={i18nTexts.certTitle}
description={i18nTexts.certDescription}
selectable={{
onClick: () => {
setSecurityModel(SECURITY_MODEL.CERTIFICATE);
},
isSelected: securityModel === SECURITY_MODEL.CERTIFICATE,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xxl" />
<EuiFlexGroup wrap justifyContent="center">
<EuiFlexItem style={{ maxWidth: CARD_MAX_WIDTH }}>
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="setupTrustBackButton"
iconType="arrowLeft"
onClick={onBack}
>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.backButtonLabel"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: CARD_MAX_WIDTH }}>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="setupTrustDoneButton"
color="primary"
fill
isLoading={isSaving}
onClick={() => setIsModalVisible(true)}
>
<FormattedMessage
id="xpack.remoteClusters.clusterWizard.trustStep.doneButtonLabel"
defaultMessage="Add remote cluster"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{isModalVisible && (
<ConfirmTrustSetupModal closeModal={() => setIsModalVisible(false)} onSubmit={onSubmit} />
)}
</EuiFlexGroup>
<ActionButtons
showRequest={false}
disabled={!securityModel}
handleNext={() => {
next(securityModel);
}}
confirmFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.nextButtonLabel"
defaultMessage="Next"
/>
}
nextButtonTestSubj={'remoteClusterTrustNextButton'}
/>
</div>
);
};

View file

@ -5,14 +5,17 @@
* 2.0.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React, { ReactNode } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { remoteClustersUrl } from '../../../services/documentation';
import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
interface Props {
title: ReactNode;
description?: ReactNode;
}
export const RemoteClusterPageTitle = ({ title, description }) => (
export const RemoteClusterPageTitle: React.FC<Props> = ({ title, description }) => (
<>
<EuiPageHeader
bottomBorder
@ -38,8 +41,3 @@ export const RemoteClusterPageTitle = ({ title, description }) => (
<EuiSpacer size="l" />
</>
);
RemoteClusterPageTitle.propTypes = {
title: PropTypes.node.isRequired,
description: PropTypes.node,
};

View file

@ -6,22 +6,23 @@
*/
import { connect } from 'react-redux';
import { ClusterPayload } from '../../../../common/lib';
import { RemoteClusterAdd as RemoteClusterAddView } from './remote_cluster_add';
import { isAddingCluster, getAddClusterError } from '../../store/selectors';
import { addCluster, clearAddClusterErrors } from '../../store/actions';
const mapStateToProps = (state) => {
const mapStateToProps = (state: any) => {
return {
isAddingCluster: isAddingCluster(state),
addClusterError: getAddClusterError(state),
};
};
const mapDispatchToProps = (dispatch) => {
const mapDispatchToProps = (dispatch: (action: any) => void) => {
return {
addCluster: (cluster) => {
addCluster: (cluster: ClusterPayload) => {
dispatch(addCluster(cluster));
},
clearAddClusterErrors: () => {
@ -30,4 +31,14 @@ const mapDispatchToProps = (dispatch) => {
};
};
export const RemoteClusterAdd = connect(mapStateToProps, mapDispatchToProps)(RemoteClusterAddView);
interface Props {
addCluster: (cluster: ClusterPayload) => void;
isAddingCluster: boolean;
addClusterError?: { message: string };
clearAddClusterErrors: () => void;
}
export const RemoteClusterAdd: React.FC<Props> = connect(
mapStateToProps,
mapDispatchToProps
)(RemoteClusterAddView);

View file

@ -1,88 +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, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPageSection, EuiPageBody } from '@elastic/eui';
import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterPageTitle } from '../components';
import { RemoteClusterWizard } from './wizard_form';
export class RemoteClusterAdd extends PureComponent {
static propTypes = {
addCluster: PropTypes.func,
isAddingCluster: PropTypes.bool,
addClusterError: PropTypes.object,
clearAddClusterErrors: PropTypes.func,
};
componentDidMount() {
setBreadcrumbs('add');
}
componentWillUnmount() {
// Clean up after ourselves.
this.props.clearAddClusterErrors();
}
save = (clusterConfig) => {
this.props.addCluster(clusterConfig);
};
redirectToList = () => {
const {
history,
route: {
location: { search },
},
} = getRouter();
const { redirect: redirectUrl } = extractQueryParams(search);
if (redirectUrl) {
const decodedRedirect = decodeURIComponent(redirectUrl);
redirect(decodedRedirect);
} else {
history.push('/list');
}
};
render() {
const { isAddingCluster, addClusterError } = this.props;
return (
<EuiPageBody data-test-subj="remote-clusters-add">
<EuiPageSection paddingSize="none">
<RemoteClusterPageTitle
title={
<FormattedMessage
id="xpack.remoteClusters.addTitle"
defaultMessage="Add remote cluster"
/>
}
description={
<FormattedMessage
id="xpack.remoteClusters.remoteClustersDescription"
defaultMessage="Create a connection from this cluster to other Elasticsearch clusters."
/>
}
/>
<RemoteClusterWizard
saveRemoteClusterConfig={this.save}
onCancel={this.redirectToList}
isSaving={isAddingCluster}
addClusterError={addClusterError}
/>
</EuiPageSection>
</EuiPageBody>
);
}
}

View file

@ -0,0 +1,85 @@
/*
* 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, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiPageSection, EuiPageBody } from '@elastic/eui';
import { ClusterPayload } from '../../../../common/lib';
import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterPageTitle } from '../components';
import { RemoteClusterWizard } from './wizard_form';
interface Props {
addCluster: (cluster: ClusterPayload) => void;
isAddingCluster: boolean;
addClusterError?: { message: string };
clearAddClusterErrors: () => void;
}
export const RemoteClusterAdd: React.FC<Props> = ({
addCluster,
isAddingCluster,
addClusterError,
clearAddClusterErrors,
}) => {
useEffect(() => {
setBreadcrumbs('add');
return () => {
// Clean up after ourselves.
clearAddClusterErrors();
};
}, [clearAddClusterErrors]);
const redirectToList = () => {
const {
route: {
location: { search },
},
history,
} = getRouter();
const { redirect: redirectUrl } = extractQueryParams(search);
if (redirectUrl && typeof redirectUrl === 'string') {
const decodedRedirect = decodeURIComponent(redirectUrl);
redirect(decodedRedirect);
} else {
history.push('/list');
}
};
return (
<EuiPageBody data-test-subj="remote-clusters-add">
<EuiPageSection paddingSize="none">
<RemoteClusterPageTitle
title={
<FormattedMessage
id="xpack.remoteClusters.addTitle"
defaultMessage="Add remote cluster"
/>
}
description={
<FormattedMessage
id="xpack.remoteClusters.remoteClustersDescription"
defaultMessage="Add a remote cluster that connects to seed nodes or to a single proxy address."
/>
}
/>
<RemoteClusterWizard
saveRemoteClusterConfig={addCluster}
onCancel={redirectToList}
isSaving={isAddingCluster}
addClusterError={addClusterError}
/>
</EuiPageSection>
</EuiPageBody>
);
};

View file

@ -5,15 +5,20 @@
* 2.0.
*/
import React, { useState, useMemo, useEffect } from 'react';
import React, { useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiStepsHorizontal, EuiStepStatus, EuiSpacer, EuiPageSection } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { RemoteClusterSetupTrust, RemoteClusterForm } from '../components';
import { ClusterPayload } from '../../../../common/lib/cluster_serialization';
import { RemoteClusterReview } from '../components/remote_cluster_config_steps/remote_cluster_review';
const CONFIGURE_CONNECTION = 1;
const SETUP_TRUST = 2;
const SETUP_TRUST = 1;
const CONFIGURE_CONNECTION = 2;
const REVIEW = 3;
const FORM_MAX_WIDTH = 850;
interface Props {
saveRemoteClusterConfig: (config: ClusterPayload) => void;
@ -29,53 +34,73 @@ export const RemoteClusterWizard = ({
addClusterError,
}: Props) => {
const [formState, setFormState] = useState<ClusterPayload>();
const [currentStep, setCurrentStep] = useState(CONFIGURE_CONNECTION);
// If there was an error saving the cluster, we need
// to send the user back to the first step.
useEffect(() => {
if (addClusterError) {
setCurrentStep(CONFIGURE_CONNECTION);
}
}, [addClusterError, setCurrentStep]);
const [formHasErrors, setFormHasErrors] = useState(false);
const [currentStep, setCurrentStep] = useState(SETUP_TRUST);
const [securityModel, setSecurityModel] = useState('');
const stepDefinitions = useMemo(
() => [
{
step: SETUP_TRUST,
title: i18n.translate('xpack.remoteClusters.clusterWizard.selectConnectionTypeLabel', {
defaultMessage: 'Select connection type',
}),
status: (currentStep === SETUP_TRUST ? 'current' : 'complete') as EuiStepStatus,
onClick: () => setCurrentStep(SETUP_TRUST),
},
{
step: CONFIGURE_CONNECTION,
title: i18n.translate('xpack.remoteClusters.clusterWizard.addConnectionInfoLabel', {
defaultMessage: 'Add connection information',
}),
disabled: !securityModel,
status: (currentStep === CONFIGURE_CONNECTION ? 'current' : 'complete') as EuiStepStatus,
onClick: () => setCurrentStep(CONFIGURE_CONNECTION),
},
{
step: SETUP_TRUST,
title: i18n.translate('xpack.remoteClusters.clusterWizard.setupTrustLabel', {
defaultMessage: 'Establish trust',
step: REVIEW,
title: i18n.translate('xpack.remoteClusters.clusterWizard.confirmSetup', {
defaultMessage: 'Confirm setup',
}),
status: (currentStep === SETUP_TRUST ? 'current' : 'incomplete') as EuiStepStatus,
disabled: !formState,
onClick: () => setCurrentStep(SETUP_TRUST),
disabled: !formState || formHasErrors,
status: (currentStep === REVIEW ? 'current' : 'incomplete') as EuiStepStatus,
onClick: () => setCurrentStep(REVIEW),
},
],
[currentStep, formState, setCurrentStep]
[currentStep, formHasErrors, formState, securityModel]
);
const completeTrustStep = (model: string) => {
setSecurityModel(model);
setCurrentStep(CONFIGURE_CONNECTION);
};
const onSecurityUpdate = (model: string) => {
if (securityModel !== '') {
setSecurityModel(model);
}
};
// Upon finalizing configuring the connection, we need to temporarily store the
// cluster configuration so that we can persist it when the user completes the
// trust step.
const completeConfigStep = (clusterConfig: ClusterPayload) => {
setFormState(clusterConfig);
setCurrentStep(SETUP_TRUST);
setCurrentStep(REVIEW);
};
const completeTrustStep = () => {
const onConfigUpdate = (clusterConfig: ClusterPayload, hasErrors: boolean) => {
if (formState !== undefined) {
setFormState(clusterConfig);
setFormHasErrors(hasErrors);
}
};
const completeReviewStep = () => {
saveRemoteClusterConfig(formState as ClusterPayload);
};
return (
<EuiPageSection restrictWidth>
<EuiPageSection restrictWidth={FORM_MAX_WIDTH}>
<EuiStepsHorizontal steps={stepDefinitions} />
<EuiSpacer size="xl" />
@ -83,21 +108,45 @@ export const RemoteClusterWizard = ({
Instead of unmounting the Form, we toggle its visibility not to lose the form
state when moving to the next step.
*/}
<div style={{ display: currentStep === CONFIGURE_CONNECTION ? 'block' : 'none' }}>
<RemoteClusterForm
save={completeConfigStep}
cancel={onCancel}
saveError={addClusterError}
/>
</div>
{currentStep === SETUP_TRUST && (
<RemoteClusterSetupTrust
onBack={() => setCurrentStep(CONFIGURE_CONNECTION)}
onSubmit={completeTrustStep}
isSaving={isSaving}
next={completeTrustStep}
currentSecurityModel={securityModel}
onSecurityChange={onSecurityUpdate}
/>
)}
<div style={{ display: currentStep === CONFIGURE_CONNECTION ? 'block' : 'none' }}>
<RemoteClusterForm
confirmFormAction={completeConfigStep}
onBack={() => setCurrentStep(SETUP_TRUST)}
onConfigChange={onConfigUpdate}
confirmFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.nextButtonLabel"
defaultMessage="Next"
/>
}
backFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.backButtonLabel"
defaultMessage="Back"
/>
}
/>
</div>
<div style={{ display: currentStep === REVIEW ? 'block' : 'none' }}>
{formState && (
<RemoteClusterReview
onBack={() => setCurrentStep(CONFIGURE_CONNECTION)}
isSaving={isSaving}
saveError={addClusterError}
onSubmit={completeReviewStep}
cluster={formState}
securityModel={securityModel}
/>
)}
</div>
</EuiPageSection>
);
};

View file

@ -6,6 +6,7 @@
*/
import { connect } from 'react-redux';
import { Cluster, ClusterPayload } from '../../../../common/lib';
import { RemoteClusterEdit as RemoteClusterEditView } from './remote_cluster_edit';
import {
@ -23,7 +24,7 @@ import {
openDetailPanel,
} from '../../store/actions';
const mapStateToProps = (state) => {
const mapStateToProps = (state: any) => {
return {
isLoading: isLoading(state),
cluster: getEditedCluster(state),
@ -32,27 +33,39 @@ const mapStateToProps = (state) => {
};
};
const mapDispatchToProps = (dispatch) => {
const mapDispatchToProps = (dispatch: (action: any) => void) => {
return {
startEditingCluster: (clusterName) => {
startEditingCluster: (clusterName: string) => {
dispatch(startEditingCluster({ clusterName }));
},
stopEditingCluster: () => {
dispatch(stopEditingCluster());
},
editCluster: (cluster) => {
editCluster: (cluster: ClusterPayload) => {
dispatch(editCluster(cluster));
},
clearEditClusterErrors: () => {
dispatch(clearEditClusterErrors());
},
openDetailPanel: (clusterName) => {
openDetailPanel: (clusterName: string) => {
dispatch(openDetailPanel({ name: clusterName }));
},
};
};
export const RemoteClusterEdit = connect(
interface Props {
isLoading: boolean;
cluster: Cluster;
startEditingCluster: (clusterName: string) => void;
stopEditingCluster: () => void;
editCluster: (cluster: ClusterPayload) => void;
isEditingCluster: boolean;
getEditClusterError?: object;
clearEditClusterErrors: () => void;
openDetailPanel: (clusterName: string) => void;
}
export const RemoteClusterEdit: React.FC<Props> = connect(
mapStateToProps,
mapDispatchToProps
)(RemoteClusterEditView);

View file

@ -1,226 +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 } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiCallOut,
EuiPageTemplate,
EuiPageSection,
EuiPageBody,
EuiSpacer,
} from '@elastic/eui';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
export class RemoteClusterEdit extends Component {
static propTypes = {
isLoading: PropTypes.bool,
cluster: PropTypes.object,
startEditingCluster: PropTypes.func,
stopEditingCluster: PropTypes.func,
editCluster: PropTypes.func,
isEditingCluster: PropTypes.bool,
getEditClusterError: PropTypes.object,
clearEditClusterErrors: PropTypes.func,
openDetailPanel: PropTypes.func,
};
constructor(props) {
super(props);
const {
match: {
params: { name },
},
} = props;
setBreadcrumbs('edit', `?cluster=${name}`);
this.state = {
clusterName: name,
};
}
componentDidMount() {
const { startEditingCluster } = this.props;
const { clusterName } = this.state;
startEditingCluster(clusterName);
}
componentWillUnmount() {
// Clean up after ourselves.
this.props.clearEditClusterErrors();
this.props.stopEditingCluster();
}
save = (clusterConfig) => {
this.props.editCluster(clusterConfig);
};
cancel = () => {
const { openDetailPanel } = this.props;
const { clusterName } = this.state;
const {
history,
route: {
location: { search },
},
} = getRouter();
const { redirect: redirectUrl } = extractQueryParams(search);
if (redirectUrl) {
const decodedRedirect = decodeURIComponent(redirectUrl);
redirect(decodedRedirect);
} else {
history.push('/list');
openDetailPanel(clusterName);
}
};
render() {
const { clusterName } = this.state;
const { isLoading, cluster, isEditingCluster, getEditClusterError } = this.props;
if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingLabel"
defaultMessage="Loading remote cluster…"
/>
</SectionLoading>
);
}
if (!cluster) {
return (
<EuiPageTemplate.EmptyPrompt
iconType="warning"
color="danger"
title={
<h2>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingErrorTitle"
defaultMessage="Error loading remote cluster"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingErrorMessage"
defaultMessage="The remote cluster ''{name}'' does not exist."
values={{ name: clusterName }}
/>
</p>
}
actions={
<EuiButton
{...reactRouterNavigate(this.props.history, '/list')}
color="danger"
iconType="arrowLeft"
flush="left"
>
<FormattedMessage
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
defaultMessage="View remote clusters"
/>
</EuiButton>
}
/>
);
}
const { isConfiguredByNode, hasDeprecatedProxySetting } = cluster;
if (isConfiguredByNode) {
return (
<EuiPageTemplate.EmptyPrompt
iconType="iInCircle"
title={
<h2>
<FormattedMessage
id="xpack.remoteClusters.edit.configuredByNodeWarningTitle"
defaultMessage="Defined in configuration"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.remoteClusters.configuredByNodeWarningBody"
defaultMessage="You can't edit or delete this remote cluster because it's defined in a node's
elasticsearch.yml configuration file."
/>
</p>
}
actions={
<EuiButton color="primary" iconType="arrowLeft" flush="left" onClick={this.cancel}>
<FormattedMessage
id="xpack.remoteClusters.edit.backToRemoteClustersButtonLabel"
defaultMessage="Back to remote clusters"
/>
</EuiButton>
}
/>
);
}
return (
<EuiPageBody restrictWidth={true} data-test-subj="remote-clusters-edit">
<EuiPageSection paddingSize="none">
<RemoteClusterPageTitle
title={
<FormattedMessage
id="xpack.remoteClusters.editTitle"
defaultMessage="Edit remote cluster"
/>
}
/>
{hasDeprecatedProxySetting ? (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.remoteClusters.edit.deprecatedSettingsTitle"
defaultMessage="Proceed with caution"
/>
}
color="warning"
iconType="help"
>
<FormattedMessage
id="xpack.remoteClusters.edit.deprecatedSettingsMessage"
defaultMessage="This remote cluster has deprecated settings that we tried to resolve. Verify all changes before saving."
/>
</EuiCallOut>
<EuiSpacer />
</>
) : null}
<RemoteClusterForm
cluster={cluster}
isSaving={isEditingCluster}
saveError={getEditClusterError}
save={this.save}
cancel={this.cancel}
/>
</EuiPageSection>
</EuiPageBody>
);
}
}

View file

@ -0,0 +1,184 @@
/*
* 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, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiCallOut,
EuiPageTemplate,
EuiPageSection,
EuiPageBody,
EuiSpacer,
} from '@elastic/eui';
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { useRouteMatch } from 'react-router-dom';
import { RequestError } from '../../../types';
import { Cluster, ClusterPayload } from '../../../../common/lib';
import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
const FORM_MAX_WIDTH = 850;
interface Props {
isLoading: boolean;
cluster: Cluster;
startEditingCluster: (clusterName: string) => void;
stopEditingCluster: () => void;
editCluster: (cluster: ClusterPayload) => void;
isEditingCluster: boolean;
getEditClusterError?: RequestError;
clearEditClusterErrors: () => void;
openDetailPanel: (clusterName: string) => void;
}
export const RemoteClusterEdit: React.FC<Props> = ({
isLoading,
cluster,
startEditingCluster,
stopEditingCluster,
editCluster,
isEditingCluster,
getEditClusterError,
clearEditClusterErrors,
openDetailPanel,
}) => {
const match = useRouteMatch<{ name: string }>();
const { name: clusterName } = match.params;
const {
history,
route: {
location: { search },
},
} = getRouter();
useEffect(() => {
setBreadcrumbs('edit', `?cluster=${clusterName}`);
startEditingCluster(clusterName);
return () => {
clearEditClusterErrors();
stopEditingCluster();
};
}, [clusterName, startEditingCluster, clearEditClusterErrors, stopEditingCluster]);
const cancel = () => {
const { redirect: redirectUrl } = extractQueryParams(search);
if (redirectUrl && typeof redirectUrl === 'string') {
const decodedRedirect = decodeURIComponent(redirectUrl);
redirect(decodedRedirect);
} else {
history.push('/list');
openDetailPanel(clusterName);
}
};
if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingLabel"
defaultMessage="Loading remote cluster…"
/>
</SectionLoading>
);
}
if (!cluster) {
return (
<EuiPageTemplate.EmptyPrompt
iconType="warning"
color="danger"
title={
<h2>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingErrorTitle"
defaultMessage="Error loading remote cluster"
/>
</h2>
}
body={
<p>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingErrorMessage"
defaultMessage="The remote cluster ''{name}'' does not exist."
values={{ name: clusterName }}
/>
</p>
}
actions={
<EuiButton {...reactRouterNavigate(history, '/list')} color="danger" iconType="arrowLeft">
<FormattedMessage
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
defaultMessage="View remote clusters"
/>
</EuiButton>
}
/>
);
}
const { hasDeprecatedProxySetting } = cluster;
return (
<EuiPageBody restrictWidth={true} data-test-subj="remote-clusters-edit">
<EuiPageSection restrictWidth={FORM_MAX_WIDTH}>
<RemoteClusterPageTitle
title={
<FormattedMessage
id="xpack.remoteClusters.editTitle"
defaultMessage="Edit remote cluster"
/>
}
/>
{hasDeprecatedProxySetting ? (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.remoteClusters.edit.deprecatedSettingsTitle"
defaultMessage="Proceed with caution"
/>
}
color="warning"
iconType="help"
>
<FormattedMessage
id="xpack.remoteClusters.edit.deprecatedSettingsMessage"
defaultMessage="This remote cluster has deprecated settings that we tried to resolve. Verify all changes before saving."
/>
</EuiCallOut>
<EuiSpacer />
</>
) : null}
<RemoteClusterForm
cluster={cluster}
isSaving={isEditingCluster}
saveError={getEditClusterError}
confirmFormAction={editCluster}
onBack={cancel}
confirmFormText={
<FormattedMessage id="xpack.remoteClusters.edit.save" defaultMessage="Save" />
}
backFormText={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
defaultMessage="Cancel"
/>
}
/>
</EuiPageSection>
</EuiPageBody>
);
};

View file

@ -9,7 +9,7 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { getSecurityModel } from '../../../../../../common/constants';
import { SECURITY_MODEL, getSecurityModel } from '../../../../../../common/constants';
import { Cluster } from '../../../../../../common/lib/cluster_serialization';
export function SecurityModel({ securityModel }: { securityModel: Cluster['securityModel'] }) {
@ -21,7 +21,7 @@ export function SecurityModel({ securityModel }: { securityModel: Cluster['secur
</EuiText>
</EuiFlexItem>
{securityModel !== 'api_key' && (
{securityModel !== SECURITY_MODEL.API && (
<EuiFlexItem grow={false} data-test-subj="authenticationTypeWarning">
<EuiIconTip
type="iInCircle"

View file

@ -12,9 +12,13 @@ export let remoteClustersUrl: string;
export let transportPortUrl: string;
export let proxyModeUrl: string;
export let proxySettingsUrl: string;
export let onPremSetupTrustWithCertUrl: string;
export let onPremSetupTrustWithApiKeyUrl: string;
export let cloudSetupTrustUrl: string;
export let apiKeys: string;
export let cloudCreateApiKey: string;
export let onPremPrerequisitesApiKey: string;
export let onPremSecurityApiKey: string;
export let onPremPrerequisitesCert: string;
export let onPremSecurityCert: string;
export function init({ links }: DocLinksStart): void {
skippingDisconnectedClustersUrl = links.ccs.skippingDisconnectedClusters;
@ -22,7 +26,11 @@ export function init({ links }: DocLinksStart): void {
transportPortUrl = links.elasticsearch.transportSettings;
proxyModeUrl = links.elasticsearch.remoteClustersProxy;
proxySettingsUrl = links.elasticsearch.remoteClusersProxySettings;
onPremSetupTrustWithCertUrl = links.elasticsearch.remoteClustersOnPremSetupTrustWithCert;
onPremSetupTrustWithApiKeyUrl = links.elasticsearch.remoteClustersOnPremSetupTrustWithApiKey;
cloudSetupTrustUrl = links.elasticsearch.remoteClustersCloudSetupTrust;
apiKeys = links.management.apiKeys;
cloudCreateApiKey = links.elasticsearch.remoteClustersCreateCloudClusterApiKey;
onPremPrerequisitesApiKey = links.elasticsearch.remoteClustersOnPremPrerequisitesApiKey;
onPremSecurityApiKey = links.elasticsearch.remoteClustersOnPremSecurityApiKey;
onPremPrerequisitesCert = links.elasticsearch.remoteClustersOnPremPrerequisitesCert;
onPremSecurityCert = links.elasticsearch.remoteClustersOnPremSecurityCert;
}

View file

@ -68,6 +68,7 @@ export class RemoteClustersUIPlugin
const isCloudEnabled: boolean = Boolean(cloud?.isCloudEnabled);
const cloudBaseUrl: string = cloud?.baseUrl ?? '';
const cloudDeploymentUrl: string = cloud?.deploymentUrl ?? '';
const { renderApp } = await import('./application');
const unmountAppCallback = await renderApp(
@ -75,6 +76,7 @@ export class RemoteClustersUIPlugin
{
isCloudEnabled,
cloudBaseUrl,
cloudDeploymentUrl,
executionContext,
canUseAPIKeyTrustModel: this.canUseApiKeyTrustModel,
},

View file

@ -7,6 +7,18 @@
export { extractQueryParams, indices, SectionLoading } from '@kbn/es-ui-shared-plugin/public';
export { useExecutionContext } from '@kbn/kibana-react-plugin/public';
export { useExecutionContext, useKibana } from '@kbn/kibana-react-plugin/public';
export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
export { ViewApiRequestFlyout } from '@kbn/es-ui-shared-plugin/public';
export {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiCard,
EuiText,
} from '@elastic/eui';

View file

@ -27,6 +27,11 @@ export interface ClientConfigType {
};
}
export interface RequestError {
message: string;
cause?: string[];
}
export type { RegisterManagementAppArgs };
export type { I18nStart };

View file

@ -14,7 +14,7 @@ import { httpServerMock, httpServiceMock, coreMock } from '@kbn/core/server/mock
import { kibanaResponseFactory } from '@kbn/core/server';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { API_BASE_PATH } from '../../../common/constants';
import { API_BASE_PATH, SECURITY_MODEL } from '../../../common/constants';
import { handleEsError } from '../../shared_imports';
@ -127,7 +127,7 @@ describe('GET remote clusters', () => {
skipUnavailable: false,
isConfiguredByNode: false,
mode: 'sniff',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
},
]);

View file

@ -11,7 +11,7 @@ import { httpServerMock, httpServiceMock, coreMock } from '@kbn/core/server/mock
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { API_BASE_PATH } from '../../../common/constants';
import { API_BASE_PATH, SECURITY_MODEL } from '../../../common/constants';
import { handleEsError } from '../../shared_imports';
@ -127,7 +127,7 @@ describe('UPDATE remote clusters', () => {
seeds: ['127.0.0.1:9300'],
skipUnavailable: true,
mode: 'sniff',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(remoteInfoMockFn).toHaveBeenCalledWith();
@ -207,7 +207,7 @@ describe('UPDATE remote clusters', () => {
name: 'test',
skipUnavailable: true,
mode: 'proxy',
securityModel: 'certificate',
securityModel: SECURITY_MODEL.CERTIFICATE,
});
expect(remoteInfoMockFn).toHaveBeenCalledWith();

View file

@ -35000,22 +35000,11 @@
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressInvalidError": "L'adresse distante n'est pas valide.",
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressRequiredError": "Une adresse distante est requise.",
"xpack.remoteClusters.clusterWizard.addConnectionInfoLabel": "Ajouter des informations de connexion",
"xpack.remoteClusters.clusterWizard.setupTrustLabel": "Établir la confiance",
"xpack.remoteClusters.clusterWizard.trustStep.apiKeyNote": "Les deux clusters doivent disposer de la version {minAllowedVersion} ou supérieure.",
"xpack.remoteClusters.clusterWizard.trustStep.backButtonLabel": "Retour",
"xpack.remoteClusters.clusterWizard.trustStep.body": "Avez-vous mis en place un système de confiance pour vous connecter à votre cluster distant ?",
"xpack.remoteClusters.clusterWizard.trustStep.docs": "Voir les instructions",
"xpack.remoteClusters.clusterWizard.trustStep.doneButtonLabel": "Ajouter un cluster distant",
"xpack.remoteClusters.clusterWizard.trustStep.modal.cancelButton": "Non, revenir en arrière",
"xpack.remoteClusters.clusterWizard.trustStep.modal.checkbox": "Oui, j'ai mis en place un système de confiance",
"xpack.remoteClusters.clusterWizard.trustStep.modal.createCluster": "Ajouter un cluster distant",
"xpack.remoteClusters.clusterWizard.trustStep.modal.title": "Confirmer votre configuration",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.description": "Accès fin aux index distants. Il vous faut une clé d'API fournie par l'administrateur du cluster distant.",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.title": "Clés d'API",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.description": "Accès complet au cluster distant. Il vous faut les certificats TLS du cluster distant.",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.title": "Certificats",
"xpack.remoteClusters.clusterWizard.trustStep.title": "Configurez un système d'authentification pour se connecter au cluster distant. Effectuez{br} cette étape en suivant nos instructions avant de continuer.",
"xpack.remoteClusters.configuredByNodeWarningBody": "Vous ne pouvez pas modifier ni supprimer ce cluster distant, car il est défini dans le fichier de configuration elasticsearch.yml d'un nœud.",
"xpack.remoteClusters.configuredByNodeWarningTitle": "Vous ne pouvez pas modifier ni supprimer ce cluster distant, car il est défini dans le fichier de configuration elasticsearch.yml d'un nœud.",
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "Connecté",
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "Non connecté",
@ -35051,8 +35040,6 @@
"xpack.remoteClusters.detailPanel.skipUnavailableNullValue": "Par défaut",
"xpack.remoteClusters.detailPanel.skipUnavailableTrueValue": "Oui",
"xpack.remoteClusters.detailPanel.statusTitle": "Statut",
"xpack.remoteClusters.edit.backToRemoteClustersButtonLabel": "Retour aux clusters distants",
"xpack.remoteClusters.edit.configuredByNodeWarningTitle": "Défini dans la configuration",
"xpack.remoteClusters.edit.deprecatedSettingsMessage": "Ce cluster comprend des paramètres déclassés que nous avons essayé de résoudre. Vérifiez toutes les modifications avant d'enregistrer.",
"xpack.remoteClusters.edit.deprecatedSettingsTitle": "Procéder avec prudence",
"xpack.remoteClusters.edit.loadingErrorMessage": "Le cluster distant \"{name}\" n'existe pas.",
@ -35104,18 +35091,14 @@
"xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage": "Les nœuds initiaux en double ne sont pas autorisés.`",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage": "Le nœud initial doit utiliser le format host:port. Exemple : 127.0.0.1:9400, localhost:9400. Les hôtes ne peuvent comprendre que des lettres, des chiffres et des tirets.",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage": "Un port est requis.",
"xpack.remoteClusters.remoteClusterForm.nextButtonLabel": "{isEditMode, select, true{Sauvegarder} other{Suivant}}",
"xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "L'adresse doit utiliser le format host:port. Exemple : 127.0.0.1:9400, localhost:9400. Les hôtes ne peuvent comprendre que des lettres, des chiffres et des tirets.",
"xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "Une adresse proxy est requise.",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "Configurez la manière de se connecter au cluster distant.",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "Utilisez les nœuds initiaux par défaut, ou passez au mode proxy.",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "Mode de connexion",
"xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "Nom unique pour le cluster.",
"xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "Nom",
"xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.portLinkText": "port",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "Si l'un des clusters distants n'est pas disponible, la demande de requête échoue. Pour éviter ceci et continuer à envoyer des requêtes aux autres clusters, activez {optionName}. {learnMoreLink}",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "En savoir plus.",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "Ignorer si indisponible",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel": "Ignorer si indisponible",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel": "Réinitialiser aux valeurs par défaut",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle": "Rendre le cluster distant facultatif",

View file

@ -34860,22 +34860,11 @@
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressInvalidError": "リモートアドレスが無効です。",
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressRequiredError": "リモートアドレスが必要です。",
"xpack.remoteClusters.clusterWizard.addConnectionInfoLabel": "接続情報を追加",
"xpack.remoteClusters.clusterWizard.setupTrustLabel": "信頼を確立",
"xpack.remoteClusters.clusterWizard.trustStep.apiKeyNote": "両方のクラスターがバージョン{minAllowedVersion}以上である必要があります。",
"xpack.remoteClusters.clusterWizard.trustStep.backButtonLabel": "戻る",
"xpack.remoteClusters.clusterWizard.trustStep.body": "リモートクラスターに接続するための信頼を設定しましたか?",
"xpack.remoteClusters.clusterWizard.trustStep.docs": "手順を表示",
"xpack.remoteClusters.clusterWizard.trustStep.doneButtonLabel": "リモートクラスターを追加",
"xpack.remoteClusters.clusterWizard.trustStep.modal.cancelButton": "いいえ。戻ります",
"xpack.remoteClusters.clusterWizard.trustStep.modal.checkbox": "はい。信頼を設定しました",
"xpack.remoteClusters.clusterWizard.trustStep.modal.createCluster": "リモートクラスターを追加",
"xpack.remoteClusters.clusterWizard.trustStep.modal.title": "構成を確認",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.description": "リモートインデックスへのきめ細かいアクセス。リモートクラスター管理者が提供するAPIキーが必要です。",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.title": "APIキー",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.description": "リモートクラスターへのフルアクセスリモートクラスターのTLS証明書が必要です。",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.title": "証明書",
"xpack.remoteClusters.clusterWizard.trustStep.title": "リモートクラスターに接続するための認証メカニズムを設定します。続行する前に、ドキュメントの手順を使用して、このステップを完了{br}してください。",
"xpack.remoteClusters.configuredByNodeWarningBody": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。",
"xpack.remoteClusters.configuredByNodeWarningTitle": "このリモートクラスターはノードの elasticsearch.yml 構成ファイルで定義されているため、編集または削除できません。",
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "接続済み",
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未接続",
@ -34911,8 +34900,6 @@
"xpack.remoteClusters.detailPanel.skipUnavailableNullValue": "デフォルト",
"xpack.remoteClusters.detailPanel.skipUnavailableTrueValue": "はい",
"xpack.remoteClusters.detailPanel.statusTitle": "ステータス",
"xpack.remoteClusters.edit.backToRemoteClustersButtonLabel": "リモートクラスターに戻る",
"xpack.remoteClusters.edit.configuredByNodeWarningTitle": "構成で定義",
"xpack.remoteClusters.edit.deprecatedSettingsMessage": "このリモートクラスターには解決を試みた非推奨設定があります。保存する前にすべての変更を検証してください。",
"xpack.remoteClusters.edit.deprecatedSettingsTitle": "十分ご注意ください",
"xpack.remoteClusters.edit.loadingErrorMessage": "リモートクラスター''{name}''が存在しません。",
@ -34964,18 +34951,14 @@
"xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage": "重複シードノードは使用できません。`",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage": "シードードはホストポートのフォーマットを使用する必要があります。例127.0.0.1:9400、localhost:9400ホストには文字、数字、ハイフンのみが使用できます。",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage": "ポートが必要です。",
"xpack.remoteClusters.remoteClusterForm.nextButtonLabel": "{isEditMode, select, true{保存} other{次へ}}",
"xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "アドレスはホスト:ポートの形式にする必要があります。例127.0.0.1:9400、localhost:9400ホストには文字、数字、ハイフンのみが使用できます。",
"xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "プロキシアドレスが必要です。",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "リモートクラスターに接続する方法を構成します。",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "既定でシードノードを使用するか、プロキシモードに切り替えます。",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "接続モード",
"xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "クラスターの固有の名前です。",
"xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名前",
"xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.portLinkText": "ポート",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "リモートクラスターのいずれかが利用不能な場合、クエリーリクエストは失敗します。これを回避して、他のクラスターにリクエストを送信し続けるには、{optionName}。{learnMoreLink}",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "詳細情報",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "利用不可の場合スキップ",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel": "利用不可の場合スキップ",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel": "デフォルトにリセット",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle": "リモートクラスターをオプションにする",

View file

@ -34953,22 +34953,11 @@
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressInvalidError": "远程地址无效。",
"xpack.remoteClusters.cloudDeploymentForm.remoteAddressRequiredError": "“远程地址”必填。",
"xpack.remoteClusters.clusterWizard.addConnectionInfoLabel": "添加连接信息",
"xpack.remoteClusters.clusterWizard.setupTrustLabel": "建立信任",
"xpack.remoteClusters.clusterWizard.trustStep.apiKeyNote": "两个集群都必须为 {minAllowedVersion} 版本或更高版本。",
"xpack.remoteClusters.clusterWizard.trustStep.backButtonLabel": "返回",
"xpack.remoteClusters.clusterWizard.trustStep.body": "是否已建立信任以连接到远程集群?",
"xpack.remoteClusters.clusterWizard.trustStep.docs": "查看说明",
"xpack.remoteClusters.clusterWizard.trustStep.doneButtonLabel": "添加远程集群",
"xpack.remoteClusters.clusterWizard.trustStep.modal.cancelButton": "否,返回",
"xpack.remoteClusters.clusterWizard.trustStep.modal.checkbox": "是,我已建立信任",
"xpack.remoteClusters.clusterWizard.trustStep.modal.createCluster": "添加远程集群",
"xpack.remoteClusters.clusterWizard.trustStep.modal.title": "确认您的配置",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.description": "远程索引的细粒度访问权限。您需要由远程集群管理员提供的 API 密钥。",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.title": "API 密钥",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.description": "远程集群的完全访问权限。您需要来自远程集群的 TLS 证书。",
"xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.title": "证书",
"xpack.remoteClusters.clusterWizard.trustStep.title": "设置身份验证机制以连接到远程集群。按照文档中的说明完成{br}此步骤,然后继续。",
"xpack.remoteClusters.configuredByNodeWarningBody": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。",
"xpack.remoteClusters.configuredByNodeWarningTitle": "您无法编辑或删除此远程集群,因为它是在节点的 elasticsearch.yml 配置文件中定义的。",
"xpack.remoteClusters.connectedStatus.connectedAriaLabel": "已连接",
"xpack.remoteClusters.connectedStatus.notConnectedAriaLabel": "未连接",
@ -35004,8 +34993,6 @@
"xpack.remoteClusters.detailPanel.skipUnavailableNullValue": "默认",
"xpack.remoteClusters.detailPanel.skipUnavailableTrueValue": "是",
"xpack.remoteClusters.detailPanel.statusTitle": "状态",
"xpack.remoteClusters.edit.backToRemoteClustersButtonLabel": "返回远程集群",
"xpack.remoteClusters.edit.configuredByNodeWarningTitle": "已在配置中定义",
"xpack.remoteClusters.edit.deprecatedSettingsMessage": "此远程集群具有我们已尝试解决的过时设置。保存之前确认所有更改。",
"xpack.remoteClusters.edit.deprecatedSettingsTitle": "谨慎操作",
"xpack.remoteClusters.edit.loadingErrorMessage": "远程集群“{name}”不存在。",
@ -35057,18 +35044,14 @@
"xpack.remoteClusters.remoteClusterForm.localSeedError.duplicateMessage": "不允许重复的种子节点。`",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidCharactersMessage": "种子节点必须使用 host:port 格式。例如127.0.0.1:9400、localhost:9400。主机只能由字母、数字和短划线构成。",
"xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage": "端口必填。",
"xpack.remoteClusters.remoteClusterForm.nextButtonLabel": "{isEditMode, select, true{保存} other{下一步}}",
"xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "地址必须使用 host:port 格式。例如127.0.0.1:9400、localhost:9400。主机只能由字母、数字和短划线构成。",
"xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "必须指定代理地址。",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "配置如何连接到远程集群。",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "默认使用种子节点或切换到代理模式。",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "连接模式",
"xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "集群的唯一名称。",
"xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名称",
"xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.portLinkText": "端口",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "如果任何远程集群都不可用,查询请求将失败。要避免此问题并继续向其他集群发送请求,请启用 {optionName}。{learnMoreLink}",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "了解详情。",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "如果不可用,则跳过",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableLabel": "如果不可用,则跳过",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableResetLabel": "重置为默认值",
"xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle": "使远程集群可选",

View file

@ -39,7 +39,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('follower index form', () => {
before(async () => {
await PageObjects.common.navigateToApp('remoteClusters');
await PageObjects.remoteClusters.createNewRemoteCluster(remoteName, 'localhost:9300');
await PageObjects.remoteClusters.createNewRemoteCluster(
remoteName,
'localhost:9300',
false
);
await es.indices.create({ index: testIndex });
await es.indices.create({ index: testLeader });
});

View file

@ -18,6 +18,11 @@ const deleteModalTitle = 'confirmModalTitleText';
const detailsTitle = 'remoteClusterDetailsFlyoutTitle';
const requestButton = 'remoteClustersRequestButton';
const requestTitle = 'remoteClusterRequestFlyoutTitle';
const selectApiKeyButton = 'setupTrustApiMode';
const trustStepNextButton = 'remoteClusterTrustNextButton';
const formStepNextButton = 'remoteClusterFormNextButton';
const addRemoteClusterButton = 'remoteClusterReviewtNextButton';
const closeFlyoutButton = 'euiFlyoutCloseButton';
interface Payload {
persistent: {
@ -80,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
describe('Remote Clusters Accessibility', () => {
beforeEach(async () => {
before(async () => {
await PageObjects.common.navigateToApp('remoteClusters');
});
@ -92,29 +97,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it('renders add remote cluster form', async () => {
await retry.waitFor('add remote cluster button to be rendered', async () => {
it('renders add remote cluster form - trust step', async () => {
await retry.waitFor('add remote cluster button to be rendered - trust step', async () => {
return testSubjects.isDisplayed(createButton);
});
await testSubjects.click(createButton);
await retry.waitFor('add remote cluster form to be rendered', async () => {
await retry.waitFor('add remote cluster form to be rendered - trust step', async () => {
return (await testSubjects.getVisibleText(pageTitle)) === 'Add remote cluster';
});
await a11y.testAppSnapshot();
});
it('renders add remote cluster form - form step', async () => {
await retry.waitFor('select api key button to be rendered - form step', async () => {
return testSubjects.isDisplayed(selectApiKeyButton);
});
await testSubjects.click(selectApiKeyButton);
await testSubjects.click(trustStepNextButton);
await retry.waitFor('next button form step to be rendered', async () => {
return testSubjects.isDisplayed(formStepNextButton);
});
await a11y.testAppSnapshot();
});
it('renders request flyout', async () => {
await retry.waitFor('add remote cluster button to be rendered', async () => {
return testSubjects.isDisplayed(createButton);
});
await testSubjects.click(createButton);
await retry.waitFor('add remote cluster form to be rendered', async () => {
return (await testSubjects.getVisibleText(pageTitle)) === 'Add remote cluster';
});
await testSubjects.click(requestButton);
await retry.waitFor('request flyout to be rendered', async () => {
return (await testSubjects.getVisibleText(requestTitle)) === 'Request';
@ -122,6 +132,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it('renders add remote cluster form - review step', async () => {
await testSubjects.click(closeFlyoutButton);
await testSubjects.setValue('remoteClusterFormNameInput', 'testRemoteCluster');
await testSubjects.setValue('remoteClusterFormSeedsInput', '1:1');
await testSubjects.click(formStepNextButton);
await retry.waitFor('add remote cluster button to be rendered', async () => {
return testSubjects.isDisplayed(addRemoteClusterButton);
});
await a11y.testAppSnapshot();
});
});
const modes = ['sniff', 'proxy'];
@ -133,6 +156,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
mode === 'sniff'
? getPayloadClusterSniffMode(clusterName)
: getPayloadClusterProxyMode(clusterName);
beforeEach(async () => {
await PageObjects.common.navigateToApp('remoteClusters');
});
before(async () => {
await esClient.cluster.putSettings({ body });
});

View file

@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('feature controls', function () {
this.tags('skipCloud');
loadTestFile(require.resolve('./remote_clusters_security'));
});
}

View file

@ -14,6 +14,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('Home page', function () {
before(async () => {
this.tags('skipCloud');
await security.testUser.setRoles(['global_ccr_role']);
await pageObjects.common.navigateToApp('remoteClusters');
});

View file

@ -12,8 +12,8 @@ import { FtrProviderContext } from '../../ftr_provider_context';
// https://www.elastic.co/guide/en/kibana/7.9/working-remote-clusters.html
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Remote Clusters app', function () {
this.tags('skipCloud');
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./remote_clusters'));
});
};

View file

@ -0,0 +1,64 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const security = getService('security');
const deployment = getService('deployment');
const pageObjects = getPageObjects(['common', 'remoteClusters']);
const testSubjects = getService('testSubjects');
const es = getService('es');
const REMOTE_CLUSTER_NAME = 'testName';
const HOST_PORT = 'test:9400';
describe('remote clusters', () => {
let isCloud: boolean;
before(async () => {
isCloud = await deployment.isCloud();
await security.testUser.setRoles(['global_ccr_role']);
await pageObjects.common.navigateToApp('remoteClusters');
await pageObjects.remoteClusters.createNewRemoteCluster(
REMOTE_CLUSTER_NAME,
HOST_PORT,
isCloud
);
});
after(async () => {
await es.cluster.putSettings({
persistent: {
cluster: {
remote: {
[REMOTE_CLUSTER_NAME]: {
mode: null,
skip_unavailable: null,
node_connections: null,
seeds: null,
server_name: null,
proxy_socket_connections: null,
proxy_address: null,
},
},
},
},
});
});
it('should add a remote cluster', async () => {
expect(await testSubjects.exists('remoteClusterDetailsFlyoutTitle')).to.be(true);
expect(await testSubjects.getVisibleText('remoteClusterDetailsFlyoutTitle')).to.be(
REMOTE_CLUSTER_NAME
);
const hostFieldId = isCloud ? 'remoteClusterDetailProxyAddress' : 'remoteClusterDetailSeeds';
expect(await testSubjects.exists(hostFieldId)).to.be(true);
expect(await testSubjects.getVisibleText(hostFieldId)).to.be(HOST_PORT);
});
});
}

View file

@ -9,34 +9,46 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function RemoteClustersPageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const retry = getService('retry');
enum ConnectionType {
'cert' = 'setupTrustCertMode',
'api' = 'setupTrustApiMode',
}
return {
async remoteClusterCreateButton() {
return await testSubjects.find('remoteClusterEmptyPromptCreateButton');
},
async createNewRemoteCluster(
name: string,
seedNode: string,
proxyMode?: boolean,
nodeConnections?: number,
skipIfUnavailable?: boolean
host: string,
isCloud: boolean,
trustMode: 'cert' | 'api' = 'cert'
) {
await (await this.remoteClusterCreateButton()).click();
await retry.waitFor('remote cluster form to be visible', async () => {
return await testSubjects.isDisplayed('remoteClusterFormNameInput');
await retry.waitFor('setup trust tab to be visible', async () => {
return await testSubjects.isDisplayed('remoteClusterTrustNextButton');
});
await testSubjects.click(ConnectionType[trustMode]);
await testSubjects.click('remoteClusterTrustNextButton');
await retry.waitFor('form tab to be visible', async () => {
return await testSubjects.isDisplayed('remoteClusterFormNextButton');
});
await testSubjects.setValue('remoteClusterFormNameInput', name);
await comboBox.setCustom('comboBoxInput', seedNode);
const hostFieldId = isCloud
? 'remoteClusterFormRemoteAddressInput'
: 'remoteClusterFormSeedsInput';
await testSubjects.setValue(hostFieldId, host);
await testSubjects.click('remoteClusterFormNextButton');
await retry.waitFor('review tab to be visible', async () => {
return await testSubjects.isDisplayed('remoteClusterReviewtNextButton');
});
// Submit config form
await testSubjects.click('remoteClusterFormSaveButton');
// Complete trust setup
await testSubjects.click('setupTrustDoneButton');
await testSubjects.setCheckbox('remoteClusterTrustCheckboxLabel', 'check');
await testSubjects.click('remoteClusterTrustSubmitButton');
await testSubjects.click('remoteClusterReviewtNextButton');
},
async getRemoteClustersList() {
const table = await testSubjects.find('remoteClusterListTable');