[Remote clusters] Add new security model (#161836)

This commit is contained in:
Ignacio Rivas 2023-08-14 11:58:53 +03:00 committed by GitHub
parent 56be6c6fb6
commit 6e241a8b02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 785 additions and 220 deletions

View file

@ -391,6 +391,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
remoteClusters: `${ELASTICSEARCH_DOCS}remote-clusters.html`,
remoteClustersProxy: `${ELASTICSEARCH_DOCS}remote-clusters.html#proxy-mode`,
remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}remote-clusters-settings.html#remote-cluster-proxy-settings`,
remoteClustersOnPremSetupTrustWithCert: `${ELASTICSEARCH_DOCS}remote-clusters-cert.html`,
remoteClustersOnPremSetupTrustWithApiKey: `${ELASTICSEARCH_DOCS}remote-clusters-api-key.html`,
remoteClustersCloudSetupTrust: `${ELASTIC_WEBSITE_URL}guide/en/cloud/current/ec-enable-ccs.html`,
rrf: `${ELASTICSEARCH_DOCS}rrf.html`,
scriptParameters: `${ELASTICSEARCH_DOCS}modules-scripting-using.html#prefer-params`,
secureCluster: `${ELASTICSEARCH_DOCS}secure-cluster.html`,

View file

@ -1,5 +1,13 @@
# Remote Clusters
## Setting up a remote cluster
* Run `yarn es snapshot --license=trial` and log into kibana
* Create a local copy of the ES snapshot with `cp -R .es/8.10.0 .es/8.10.0-2`
* Start your local copy of the ES snapshot .es/8.10.0-2/bin/elasticsearch -E cluster.name=europe -E transport.port=9400
* Create a remote cluster using `127.0.0.1:9400` as seed node and proceed with the wizard
* Verify that your newly created remote cluster is shown as connected in the remote clusters list
## About
This plugin helps users manage their [remote clusters](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-remote-clusters.html), which enable cross-cluster search and cross-cluster replication.

View file

@ -179,6 +179,63 @@ describe('Create Remote cluster', () => {
});
});
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);
});
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('back button goes to first step', async () => {
await actions.setupTrust.backToFirstStepClick();
expect(actions.isOnFirstStep()).toBe(true);
});
test('shows only cert based config if API key trust model is not available', async () => {
await act(async () => {
({ actions, component } = await setup(httpSetup, {
canUseAPIKeyTrustModel: false,
}));
});
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);
});
});
describe('on prem', () => {
beforeEach(async () => {
await act(async () => {

View file

@ -49,10 +49,21 @@ export interface RemoteClustersActions {
getLabel: () => string;
exists: () => boolean;
};
isOnFirstStep: () => boolean;
saveButton: {
click: () => void;
isDisabled: () => boolean;
};
setupTrust: {
isSubmitInConfirmDisabled: () => boolean;
toggleConfirmSwitch: () => void;
setupTrustConfirmClick: () => void;
backToFirstStepClick: () => void;
apiCardExist: () => boolean;
certCardExist: () => boolean;
apiCardDocsExist: () => boolean;
certCardDocsExist: () => boolean;
};
getErrorMessages: () => string[];
globalErrorExists: () => boolean;
}
@ -148,7 +159,7 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct
};
};
const createSaveButtonActions = () => {
const createSetupTrustActions = () => {
const click = () => {
act(() => {
find('remoteClusterFormSaveButton').simulate('click');
@ -157,7 +168,56 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct
component.update();
};
const isDisabled = () => find('remoteClusterFormSaveButton').props().disabled;
return { saveButton: { click, isDisabled } };
const setupTrustConfirmClick = () => {
act(() => {
find('setupTrustDoneButton').simulate('click');
});
component.update();
};
const backToFirstStepClick = () => {
act(() => {
find('setupTrustBackButton').simulate('click');
});
component.update();
};
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 isSubmitInConfirmDisabled = () => find('remoteClusterTrustSubmitButton').props().disabled;
const apiCardExist = () => exists('setupTrustApiKeyCard');
const certCardExist = () => exists('setupTrustCertCard');
const apiCardDocsExist = () => exists('setupTrustApiKeyCardDocs');
const certCardDocsExist = () => exists('setupTrustCertCardDocs');
return {
isOnFirstStep,
saveButton: { click, isDisabled },
setupTrust: {
setupTrustConfirmClick,
isSubmitInConfirmDisabled,
toggleConfirmSwitch,
apiCardExist,
certCardExist,
apiCardDocsExist,
certCardDocsExist,
backToFirstStepClick,
},
};
};
const createServerNameActions = () => {
@ -192,7 +252,7 @@ export const createRemoteClustersActions = (testBed: TestBed): RemoteClustersAct
...createCloudUrlInputActions(),
...createProxyAddressActions(),
...createServerNameActions(),
...createSaveButtonActions(),
...createSetupTrustActions(),
getErrorMessages: form.getErrorsMessages,
globalErrorExists,
};

View file

@ -35,6 +35,7 @@ export const WithAppDependencies =
isCloudEnabled: !!isCloudEnabled,
cloudBaseUrl: 'test.com',
executionContext: executionContextServiceMock.createStartContract(),
canUseAPIKeyTrustModel: true,
...overrides,
}}
>

View file

@ -12,6 +12,7 @@ export interface Context {
isCloudEnabled: boolean;
cloudBaseUrl: string;
executionContext: ExecutionContextStart;
canUseAPIKeyTrustModel: boolean;
}
export const AppContext = createContext<Context>({} as any);

View file

@ -16,6 +16,7 @@ export declare const renderApp: (
isCloudEnabled: boolean;
cloudBaseUrl: string;
executionContext: ExecutionContextStart;
canUseAPIKeyTrustModel: boolean;
},
history: ScopedHistory,
theme$: Observable<CoreTheme>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export { RemoteClusterSetupTrust } from './remote_cluster_setup_trust';
export { RemoteClusterForm } from './remote_cluster_form';
export { RemoteClusterPageTitle } from './remote_cluster_page_title';
export { ConfiguredByNodeWarning } from './configured_by_node_warning';

View file

@ -21,11 +21,9 @@ import {
EuiFormRow,
EuiLink,
EuiLoadingLogo,
EuiLoadingSpinner,
EuiOverlayMask,
EuiSpacer,
EuiSwitch,
EuiText,
EuiTitle,
EuiDelayRender,
EuiScreenReaderOnly,
@ -302,87 +300,67 @@ export class RemoteClusterForm extends Component<Props, State> {
}
renderActions() {
const { isSaving, cancel } = this.props;
const { isSaving, cancel, cluster: isEditMode } = this.props;
const { areErrorsVisible, isRequestVisible } = this.state;
if (isSaving) {
return (
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.actions.savingText"
defaultMessage="Saving"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
let cancelButton;
if (cancel) {
cancelButton = (
<EuiFlexItem grow={false}>
<EuiButtonEmpty color="primary" onClick={cancel}>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
);
}
const isSaveDisabled = areErrorsVisible && this.hasErrors();
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="success"
iconType="check"
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.saveButtonLabel"
defaultMessage="Save"
id="xpack.remoteClusters.remoteClusterForm.nextButtonLabel"
defaultMessage="{isEditMode, select, true{Save} other{Next}}"
values={{
isEditMode: Boolean(isEditMode),
}}
/>
</EuiButton>
</EuiFlexItem>
{cancelButton}
</EuiFlexGroup>
</EuiFlexItem>
<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>
</EuiFlexGroup>
);
}
@ -523,20 +501,20 @@ export class RemoteClusterForm extends Component<Props, State> {
return (
<Fragment>
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
<EuiCallOut
title={
<h3 id={this.generateId(ERROR_TITLE_ID)}>
<span id={this.generateId(ERROR_TITLE_ID)}>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
defaultMessage="Fix errors before continuing."
defaultMessage="Some fields require your attention."
/>
</h3>
</span>
}
color="danger"
iconType="cross"
iconType="error"
/>
<EuiDelayRender>{messagesToBeRendered}</EuiDelayRender>
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
</Fragment>
);
};
@ -549,6 +527,7 @@ export class RemoteClusterForm extends Component<Props, State> {
return (
<Fragment>
{this.renderSaveErrorFeedback()}
{this.renderErrors()}
<EuiForm data-test-subj="remoteClusterForm">
<EuiDescribedFormGroup
@ -609,8 +588,6 @@ export class RemoteClusterForm extends Component<Props, State> {
{this.renderSkipUnavailable()}
</EuiForm>
{this.renderErrors()}
<EuiSpacer size="l" />
{this.renderActions()}

View file

@ -0,0 +1,104 @@
/*
* 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',
})}
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

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

View file

@ -0,0 +1,197 @@
/*
* 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, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiSpacer,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import * as docs from '../../../services/documentation';
import { AppContext } from '../../../app_context';
import { ConfirmTrustSetupModal } from './confirm_modal';
const MIN_ALLOWED_VERSION_API_KEYS_METHOD = '8.10';
const CARD_MAX_WIDTH = 400;
const i18nTexts = {
apiKeyTitle: i18n.translate(
'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.title',
{ defaultMessage: 'API keys' }
),
apiKeyBadge: i18n.translate(
'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.badge',
{ defaultMessage: 'BETA' }
),
apiKeyDescription: i18n.translate(
'xpack.remoteClusters.clusterWizard.trustStep.setupWithApiKeys.description',
{
defaultMessage:
'Fine-grained access to remote indices. You need an API key provided by the remote cluster administrator.',
}
),
certTitle: i18n.translate('xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.title', {
defaultMessage: 'Certificates',
}),
certDescription: i18n.translate(
'xpack.remoteClusters.clusterWizard.trustStep.setupWithCert.description',
{
defaultMessage:
'Full access to the remote cluster. You need TLS certificates from the remote cluster.',
}
),
};
const docLinks = {
cert: docs.onPremSetupTrustWithCertUrl,
apiKey: docs.onPremSetupTrustWithApiKeyUrl,
cloud: docs.cloudSetupTrustUrl,
};
interface Props {
onBack: () => void;
onSubmit: () => void;
isSaving: boolean;
}
export const RemoteClusterSetupTrust = ({ onBack, onSubmit, isSaving }: Props) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const { canUseAPIKeyTrustModel, isCloudEnabled } = useContext(AppContext);
return (
<div>
<EuiText size="m" textAlign="center">
<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 />,
}}
/>
</p>
</EuiText>
<EuiSpacer size="xxl" />
<EuiFlexGroup wrap justifyContent="center">
{canUseAPIKeyTrustModel && (
<EuiFlexItem style={{ maxWidth: CARD_MAX_WIDTH }}>
<EuiCard
title={i18nTexts.apiKeyTitle}
paddingSize="l"
betaBadgeProps={{ label: i18nTexts.apiKeyBadge, color: 'accent' }}
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>
</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>
</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>
</div>
);
};

View file

@ -8,11 +8,13 @@
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, RemoteClusterForm } from '../components';
import { RemoteClusterPageTitle } from '../components';
import { RemoteClusterWizard } from './wizard_form';
export class RemoteClusterAdd extends PureComponent {
static propTypes = {
@ -35,7 +37,7 @@ export class RemoteClusterAdd extends PureComponent {
this.props.addCluster(clusterConfig);
};
cancel = () => {
redirectToList = () => {
const {
history,
route: {
@ -56,29 +58,31 @@ export class RemoteClusterAdd extends PureComponent {
const { isAddingCluster, addClusterError } = this.props;
return (
<>
<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."
/>
}
/>
<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."
/>
}
/>
<RemoteClusterForm
isSaving={isAddingCluster}
saveError={addClusterError}
save={this.save}
cancel={this.cancel}
/>
</>
<RemoteClusterWizard
saveRemoteClusterConfig={this.save}
onCancel={this.redirectToList}
isSaving={isAddingCluster}
addClusterError={addClusterError}
/>
</EuiPageSection>
</EuiPageBody>
);
}
}

View file

@ -0,0 +1,103 @@
/*
* 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, useMemo, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiStepsHorizontal, EuiStepStatus, EuiSpacer, EuiPageSection } from '@elastic/eui';
import { RemoteClusterSetupTrust, RemoteClusterForm } from '../components';
import { Cluster } from '../../../../common/lib/cluster_serialization';
const CONFIGURE_CONNECTION = 1;
const SETUP_TRUST = 2;
interface Props {
saveRemoteClusterConfig: (config: Cluster) => void;
onCancel: () => void;
addClusterError: { message: string } | undefined;
isSaving: boolean;
}
export const RemoteClusterWizard = ({
saveRemoteClusterConfig,
onCancel,
isSaving,
addClusterError,
}: Props) => {
const [formState, setFormState] = useState<Cluster>();
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 stepDefinitions = useMemo(
() => [
{
step: CONFIGURE_CONNECTION,
title: i18n.translate('xpack.remoteClusters.clusterWizard.addConnectionInfoLabel', {
defaultMessage: 'Add connection information',
}),
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',
}),
status: (currentStep === SETUP_TRUST ? 'current' : 'incomplete') as EuiStepStatus,
disabled: !formState,
onClick: () => setCurrentStep(SETUP_TRUST),
},
],
[currentStep, formState, setCurrentStep]
);
// 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: Cluster) => {
setFormState(clusterConfig);
setCurrentStep(SETUP_TRUST);
};
const completeTrustStep = () => {
saveRemoteClusterConfig(formState as Cluster);
};
return (
<EuiPageSection restrictWidth>
<EuiStepsHorizontal steps={stepDefinitions} />
<EuiSpacer size="xl" />
{/*
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}
/>
)}
</EuiPageSection>
);
};

View file

@ -12,8 +12,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiCallOut,
EuiEmptyPrompt,
EuiPageContent_Deprecated as EuiPageContent,
EuiPageTemplate,
EuiPageSection,
EuiPageBody,
EuiSpacer,
} from '@elastic/eui';
@ -95,22 +96,23 @@ export class RemoteClusterEdit extends Component {
if (isLoading) {
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="subdued">
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<SectionLoading>
<FormattedMessage
id="xpack.remoteClusters.edit.loadingLabel"
defaultMessage="Loading remote cluster…"
/>
</SectionLoading>
</EuiPageContent>
</EuiPageTemplate>
);
}
if (!cluster) {
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger">
<EuiEmptyPrompt
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<EuiPageTemplate.EmptyPrompt
iconType="warning"
color="danger"
title={
<h2>
<FormattedMessage
@ -142,7 +144,7 @@ export class RemoteClusterEdit extends Component {
</EuiButton>
}
/>
</EuiPageContent>
</EuiPageTemplate>
);
}
@ -150,8 +152,8 @@ export class RemoteClusterEdit extends Component {
if (isConfiguredByNode) {
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="primary">
<EuiEmptyPrompt
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<EuiPageTemplate.EmptyPrompt
iconType="iInCircle"
title={
<h2>
@ -179,50 +181,52 @@ export class RemoteClusterEdit extends Component {
</EuiButton>
}
/>
</EuiPageContent>
</EuiPageTemplate>
);
}
return (
<>
<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"
>
<EuiPageBody restrictWidth={true} data-test-subj="remote-clusters-edit">
<EuiPageSection paddingSize="none">
<RemoteClusterPageTitle
title={
<FormattedMessage
id="xpack.remoteClusters.edit.deprecatedSettingsMessage"
defaultMessage="This remote cluster has deprecated settings that we tried to resolve. Verify all changes before saving."
id="xpack.remoteClusters.editTitle"
defaultMessage="Edit remote cluster"
/>
</EuiCallOut>
<EuiSpacer />
</>
) : null}
}
/>
<RemoteClusterForm
cluster={cluster}
isSaving={isEditingCluster}
saveError={getEditClusterError}
save={this.save}
cancel={this.cancel}
/>
</>
{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

@ -9,28 +9,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiText } from '@elastic/eui';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants';
export function ConnectionStatus({ isConnected, mode }) {
let icon;
let message;
if (isConnected) {
icon = <EuiIcon type="check" color="success" />;
message = i18n.translate('xpack.remoteClusters.connectedStatus.connectedAriaLabel', {
defaultMessage: 'Connected',
});
} else {
icon = <EuiIcon type="cross" color="danger" />;
message = i18n.translate('xpack.remoteClusters.connectedStatus.notConnectedAriaLabel', {
defaultMessage: 'Not connected',
});
}
const seedNodeTooltip = i18n.translate(
'xpack.remoteClusters.connectedStatus.notConnectedToolTip',
{
@ -41,15 +24,18 @@ export function ConnectionStatus({ isConnected, mode }) {
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<span data-test-subj="remoteClusterConnectionStatusIcon" className="eui-displayBlock">
{icon}
</span>
</EuiFlexItem>
<EuiFlexItem grow={false} className="remoteClustersConnectionStatus__message">
<EuiText data-test-subj="remoteClusterConnectionStatusMessage" size="s">
{message}
</EuiText>
<EuiBadge
color={isConnected ? 'success' : 'danger'}
data-test-subj="remoteClusterConnectionStatusMessage"
>
{isConnected
? i18n.translate('xpack.remoteClusters.connectedStatus.connectedAriaLabel', {
defaultMessage: 'Connected',
})
: i18n.translate('xpack.remoteClusters.connectedStatus.notConnectedAriaLabel', {
defaultMessage: 'Not connected',
})}
</EuiBadge>
</EuiFlexItem>
{!isConnected && mode === SNIFF_MODE && (

View file

@ -12,12 +12,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiButtonEmpty,
EuiEmptyPrompt,
EuiLoadingLogo,
EuiOverlayMask,
EuiPageContent_Deprecated as EuiPageContent,
EuiSpacer,
EuiPageHeader,
EuiPageSection,
EuiPageBody,
EuiPageTemplate,
EuiTitle,
EuiLink,
} from '@elastic/eui';
import { remoteClustersUrl } from '../../services/documentation';
@ -90,9 +93,10 @@ export class RemoteClusterList extends Component {
renderNoPermission() {
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger">
<EuiEmptyPrompt
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<EuiPageTemplate.EmptyPrompt
iconType="warning"
color="danger"
title={
<h2>
<FormattedMessage
@ -110,7 +114,7 @@ export class RemoteClusterList extends Component {
</p>
}
/>
</EuiPageContent>
</EuiPageTemplate>
);
}
@ -120,9 +124,10 @@ export class RemoteClusterList extends Component {
const { statusCode, error: errorString } = error.body;
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger">
<EuiEmptyPrompt
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<EuiPageTemplate.EmptyPrompt
iconType="warning"
color="danger"
title={
<h2>
<FormattedMessage
@ -137,14 +142,14 @@ export class RemoteClusterList extends Component {
</p>
}
/>
</EuiPageContent>
</EuiPageTemplate>
);
}
renderEmpty() {
return (
<EuiPageContent verticalPosition="center" horizontalPosition="center" color="subdued">
<EuiEmptyPrompt
<EuiPageTemplate minHeight={0} panelled paddingSize="none" offset={0}>
<EuiPageTemplate.EmptyPrompt
data-test-subj="remoteClusterListEmptyPrompt"
iconType="managementApp"
title={
@ -177,17 +182,36 @@ export class RemoteClusterList extends Component {
/>
</EuiButton>
}
footer={
<>
<EuiTitle size="xxs">
<span>
<FormattedMessage
id="xpack.remoteClusters.remoteClusters.emptyState.docsDescription"
defaultMessage="Want to learn more?"
/>
</span>
</EuiTitle>{' '}
<EuiLink href={remoteClustersUrl} target="_blank">
<FormattedMessage
id="xpack.remoteClusters.remoteClusters.emptyState.docsLink"
defaultMessage="Read documentation"
/>
</EuiLink>
</>
}
/>
</EuiPageContent>
</EuiPageTemplate>
);
}
renderLoading() {
return (
<EuiPageContent
verticalPosition="center"
horizontalPosition="center"
color="subdued"
<EuiPageTemplate
minHeight={0}
panelled
paddingSize="none"
offset={0}
data-test-subj="remoteClustersTableLoading"
>
<SectionLoading>
@ -196,7 +220,7 @@ export class RemoteClusterList extends Component {
defaultMessage="Loading remote clusters…"
/>
</SectionLoading>
</EuiPageContent>
</EuiPageTemplate>
);
}
@ -204,35 +228,37 @@ export class RemoteClusterList extends Component {
const { clusters } = this.props;
return (
<>
<EuiPageHeader
bottomBorder
pageTitle={
<FormattedMessage
id="xpack.remoteClusters.remoteClusterListTitle"
defaultMessage="Remote Clusters"
/>
}
rightSideItems={[
<EuiButtonEmpty
href={remoteClustersUrl}
target="_blank"
iconType="help"
data-test-subj="documentationLink"
>
<EuiPageBody data-test-subj="remote-clusters-list">
<EuiPageSection paddingSize="none">
<EuiPageHeader
bottomBorder
pageTitle={
<FormattedMessage
id="xpack.remoteClusters.remoteClustersDocsLinkText"
defaultMessage="Remote Clusters docs"
id="xpack.remoteClusters.remoteClusterListTitle"
defaultMessage="Remote Clusters"
/>
</EuiButtonEmpty>,
]}
/>
}
rightSideItems={[
<EuiButtonEmpty
href={remoteClustersUrl}
target="_blank"
iconType="help"
data-test-subj="documentationLink"
>
<FormattedMessage
id="xpack.remoteClusters.remoteClustersDocsLinkText"
defaultMessage="Remote Clusters docs"
/>
</EuiButtonEmpty>,
]}
/>
<EuiSpacer size="l" />
<EuiSpacer size="l" />
<RemoteClusterTable clusters={clusters} />
<DetailPanel />
</>
<RemoteClusterTable clusters={clusters} />
<DetailPanel />
</EuiPageSection>
</EuiPageBody>
);
}
@ -256,7 +282,6 @@ export class RemoteClusterList extends Component {
} else {
content = this.renderList();
}
return (
<>
{content}

View file

@ -235,7 +235,7 @@ export class RemoteClusterTable extends Component {
name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.addressesColumnTitle', {
defaultMessage: 'Addresses',
}),
dataTestSubj: 'remoteClustersAddress',
'data-test-subj': 'remoteClustersAddress',
truncateText: true,
render: (mode, { seeds, proxyAddress }) => {
const clusterAddressString = mode === PROXY_MODE ? proxyAddress : seeds.join(', ');
@ -259,6 +259,7 @@ export class RemoteClusterTable extends Component {
),
sortable: true,
width: '160px',
align: 'right',
render: (mode, { connectedNodesCount, connectedSocketsCount }) => {
const remoteNodesCount =
mode === PROXY_MODE ? connectedSocketsCount : connectedNodesCount;

View file

@ -12,6 +12,9 @@ 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 function init({ links }: DocLinksStart): void {
skippingDisconnectedClustersUrl = links.ccs.skippingDisconnectedClusters;
@ -19,4 +22,7 @@ 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;
}

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public';
import { Subscription } from 'rxjs';
import { PLUGIN } from '../common/constants';
import { init as initBreadcrumbs } from './application/services/breadcrumb';
@ -27,6 +28,9 @@ export class RemoteClustersUIPlugin
{
constructor(private readonly initializerContext: PluginInitializerContext) {}
private canUseApiKeyTrustModel: boolean = false;
private licensingSubscription?: Subscription;
setup(
{ notifications: { toasts }, http, getStartServices }: CoreSetup,
{ management, usageCollection, cloud, share }: Dependencies
@ -70,7 +74,12 @@ export class RemoteClustersUIPlugin
const unmountAppCallback = await renderApp(
element,
i18nContext,
{ isCloudEnabled, cloudBaseUrl, executionContext },
{
isCloudEnabled,
cloudBaseUrl,
executionContext,
canUseAPIKeyTrustModel: this.canUseApiKeyTrustModel,
},
history,
theme$
);
@ -94,7 +103,7 @@ export class RemoteClustersUIPlugin
};
}
start({ application }: CoreStart) {
start({ application }: CoreStart, { licensing }: Dependencies) {
const {
ui: { enabled: isRemoteClustersUiEnabled },
} = this.initializerContext.config.get<ClientConfigType>();
@ -102,7 +111,13 @@ export class RemoteClustersUIPlugin
if (isRemoteClustersUiEnabled) {
initRedirect(application.navigateToApp);
}
this.licensingSubscription = licensing.license$.subscribe((next) => {
this.canUseApiKeyTrustModel = next.hasAtLeast('enterprise');
});
}
stop() {}
stop() {
this.licensingSubscription?.unsubscribe();
}
}

View file

@ -11,12 +11,14 @@ import { RegisterManagementAppArgs } from '@kbn/management-plugin/public';
import { SharePluginSetup } from '@kbn/share-plugin/public';
import { I18nStart } from '@kbn/core/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
export interface Dependencies {
management: ManagementSetup;
usageCollection: UsageCollectionSetup;
cloud: CloudSetup;
share: SharePluginSetup;
licensing: LicensingPluginStart;
}
export interface ClientConfigType {

View file

@ -28422,7 +28422,6 @@
"xpack.remoteClusters.listBreadcrumbTitle": "Clusters distants",
"xpack.remoteClusters.readDocsButtonLabel": "Documents du cluster distant",
"xpack.remoteClusters.refreshAction.errorTitle": "Erreur lors de l'actualisation des clusters distants",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "Enregistrement",
"xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "Un port est requis.",
"xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "Annuler",
"xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "Besoin d'aide ?",
@ -28458,7 +28457,6 @@
"xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "Entrer manuellement l'adresse proxy et le nom du serveur",
"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.saveButtonLabel": "Enregistrer",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "Configurez automatiquement le cluster distant à l'aide de l'URL de point de terminaison Elasticsearch du déploiement distant ou entrez l'adresse proxy et le nom du serveur manuellement.",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "Utilisez les nœuds initiaux par défaut, ou passez au mode proxy.",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "Mode de connexion",

View file

@ -28422,7 +28422,6 @@
"xpack.remoteClusters.listBreadcrumbTitle": "リモートクラスター",
"xpack.remoteClusters.readDocsButtonLabel": "リモートクラスタードキュメント",
"xpack.remoteClusters.refreshAction.errorTitle": "リモートクラスターの更新中にエラーが発生",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "保存中",
"xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "ポートが必要です。",
"xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "キャンセル",
"xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "ヘルプが必要な場合",
@ -28458,7 +28457,6 @@
"xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "手動でプロキシアドレスとサーバー名を入力",
"xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "アドレスはホスト:ポートの形式にする必要があります。例127.0.0.1:9400、localhost:9400ホストには文字、数字、ハイフンのみが使用できます。",
"xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "プロキシアドレスが必要です。",
"xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "リモートデプロイのElasticsearchエンドポイントURLを使用して、リモートクラスターを自動的に構成するか、プロキシアドレスとサーバー名を手動で入力します。",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "既定でシードノードを使用するか、プロキシモードに切り替えます。",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "接続モード",

View file

@ -28420,7 +28420,6 @@
"xpack.remoteClusters.listBreadcrumbTitle": "远程集群",
"xpack.remoteClusters.readDocsButtonLabel": "远程集群文档",
"xpack.remoteClusters.refreshAction.errorTitle": "刷新远程集群时出错",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "正在保存",
"xpack.remoteClusters.remoteClusterForm.addressError.invalidPortMessage": "端口必填。",
"xpack.remoteClusters.remoteClusterForm.cancelButtonLabel": "取消",
"xpack.remoteClusters.remoteClusterForm.cloudUrlHelp.buttonLabel": "需要帮助?",
@ -28456,7 +28455,6 @@
"xpack.remoteClusters.remoteClusterForm.manualModeFieldLabel": "手动输入代理地址和服务器名称",
"xpack.remoteClusters.remoteClusterForm.proxyError.invalidCharactersMessage": "地址必须使用 host:port 格式。例如127.0.0.1:9400、localhost:9400。主机只能由字母、数字和短划线构成。",
"xpack.remoteClusters.remoteClusterForm.proxyError.missingProxyMessage": "必须指定代理地址。",
"xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存",
"xpack.remoteClusters.remoteClusterForm.sectionModeCloudDescription": "通过使用远程部署的 Elasticsearch 终端 URL 自动配置运程集群或手动输入代理地址和服务器名称。",
"xpack.remoteClusters.remoteClusterForm.sectionModeDescription": "默认使用种子节点或切换到代理模式。",
"xpack.remoteClusters.remoteClusterForm.sectionModeTitle": "连接模式",

View file

@ -29,7 +29,14 @@ export function RemoteClustersPageProvider({ getService }: FtrProviderContext) {
});
await testSubjects.setValue('remoteClusterFormNameInput', name);
await comboBox.setCustom('comboBoxInput', seedNode);
// Submit config form
await testSubjects.click('remoteClusterFormSaveButton');
// Complete trust setup
await testSubjects.click('setupTrustDoneButton');
await testSubjects.setCheckbox('remoteClusterTrustCheckbox', 'check');
await testSubjects.click('remoteClusterTrustSubmitButton');
},
async getRemoteClustersList() {
const table = await testSubjects.find('remoteClusterListTable');