Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Remote clusters] Cloud deployment form when adding new cluster #93953

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

import React, { createContext } from 'react';
import React, { createContext, useContext } from 'react';

export interface Context {
isCloudEnabled: boolean;
cloudDeploymentUrl: string;
}

export const AppContext = createContext<Context>({} as any);
Expand All @@ -22,3 +23,10 @@ export const AppContextProvider = ({
}) => {
return <AppContext.Provider value={context}>{children}</AppContext.Provider>;
};

export const useAppContext = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('Cannot use outside of app context');

return ctx;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export declare const renderApp: (
I18nContext: I18nStart['Context'],
appDependencies: {
isCloudEnabled?: boolean;
cloudDeploymentUrl: string;
},
history: ScopedHistory
) => ReturnType<RegisterManagementAppArgs['mount']>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FunctionComponent, useState } from 'react';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiAccordion,
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiFormRow,
EuiIcon,
EuiImage,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { useAppContext } from '../../../app_context';
import {
convertCloudUrlToProxyConnection,
validateCloudUrl,
} from '../../../services/cloud_deployment_url';

import { cloudRemoteClustersUrl } from '../../../services/documentation';

// @ts-ignore
import Screenshot from './cloud_deployment_screenshot.png';

interface Props {
onClose: () => void;
onClusterConfigure: (value: { proxyAddress: string; serverName: string }) => void;
}

// TODO copy review
export const CloudDeploymentForm: FunctionComponent<Props> = ({ onClose, onClusterConfigure }) => {
const [cloudDeploymentUrl, setCloudDeploymentUrl] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const { cloudDeploymentUrl: accountLink } = useAppContext();

const configureCluster = () => {
const urlError: string | null = validateCloudUrl(cloudDeploymentUrl);
if (urlError) {
setError(urlError);
} else {
onClose();
onClusterConfigure(convertCloudUrlToProxyConnection(cloudDeploymentUrl));
}
};
return (
<EuiFlyout onClose={() => onClose()}>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiIcon type="logoCloud" size="l" />
</EuiFlexItem>

<EuiFlexItem>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.formTitle"
defaultMessage=" Add Cloud deployment as remote cluster"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be "Add Cloud deployment as a remote cluster", no?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should it say "Elastic Cloud"?

/>
</EuiFlexItem>
</EuiFlexGroup>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.formDescription"
defaultMessage="If you're connecting to a Cloud deployment, you can copy and paste the Elasticsearch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know this is cloud deployment (we don't support any other option) so I think the text can be more instructive.

Text suggestion: "Copy and paste the Elasticsearch endpoint URL of the remote deployment to automatically configure the remote cluster. The URL can be found on the remote deployment overview page."

endpoint URL into the field below. The remote cluster will be automatically configured to connect to the Cloud deployment."
/>
</p>
</EuiText>

<EuiSpacer />

<EuiAccordion
id="cloudDeploymentScreenshot"
buttonContent={i18n.translate(
'xpack.remoteClusters.cloudDeploymentForm.screenshotButtonLabel',
{ defaultMessage: 'Expand for a screenshot' }
)}
>
<EuiImage url={Screenshot} alt="Screenshot of Cloud deployment url link" />
</EuiAccordion>

<EuiSpacer />

<EuiFormRow
fullWidth={true}
isInvalid={!!error}
error={error}
label={
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.inputLabel"
defaultMessage="Elasticsearch endpoint URL"
/>
}
>
<EuiFieldText
isInvalid={!!error}
fullWidth={true}
value={cloudDeploymentUrl}
onChange={(e) => setCloudDeploymentUrl(e.target.value)}
/>
</EuiFormRow>

<EuiSpacer />

<EuiText color="subdued">
<p>
<i>
<strong>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.aliasNoteLabel"
defaultMessage="Note: "
/>
</strong>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.aliasNoteDescription"
defaultMessage="If you configured a deployment alias in Elastic Cloud,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If you configured a deployment alias, either in Elastic Cloud, in your reverse proxy or load-balancer, you will need to copy-paste the Proxy address and Server name or the remote deployment in the remote cluster form directly. These values can be copied from the Remote parameters section on the remote deployment Security page. More information is available here".

As you mentioned, we can't point to the relevant security page so I think we need to only keep the link to our user guide.

@shubhaat are we going to call this feature deployment alias or configurable deployment endpoints?

Copy link

@shubhaat shubhaat Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the UI it is called an "Custom endpoint alias", we did not use the "deployment alias" terminology since at the time this was designed we still had the CCS template that used the "deployment alias" term

Screen Shot 2021-03-09 at 7 58 41 AM

I suggest using "Custom endpoint alias" instead for consistency.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @shubhaat !

Heads up @yuliacech @cjcenizal et al that when using the alias in the URL and changing the alias things will break. That's expected since changing the alias requires changing clients, integrations, etc. Just want to raise it in the context of a need to handle what happens when we can't connect to the cluster during and after creation.

One of the benefits of using a UUID is that it never changes.

in your own load balancer or reverse proxy, you will need to copy the deployment
proxy address and server name from the Remote cluster parameters section in
the {securityPageLink} and paste them in the form directly. "
values={{
securityPageLink: (
<EuiLink href={`${accountLink}${'/security'}`} target="_blank" external={true}>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.cloudDeploymentLink"
defaultMessage="Security page"
/>
</EuiLink>
),
}}
/>
<EuiLink href={cloudRemoteClustersUrl} target="_blank" external={true}>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.aliasDocsLink"
defaultMessage="Learn more"
/>
</EuiLink>
</i>
</p>
</EuiText>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" flush="left" onClick={onClose}>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill color="primary" onClick={configureCluster}>
<FormattedMessage
id="xpack.remoteClusters.cloudDeploymentForm.configureClusterButtonLabel"
defaultMessage="Configure cluster"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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 { CloudDeploymentForm } from './cloud_deployment_form';
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export { RemoteClusterForm } from './remote_cluster_form';
export { RemoteClusterPageTitle } from './remote_cluster_page_title';
export { ConfiguredByNodeWarning } from './configured_by_node_warning';
export { CloudDeploymentForm } from './cloud_deployment_form';
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants';

import { AppContext } from '../../../app_context';
import { CloudDeploymentForm } from '../cloud_deployment_form';

const defaultFields = {
name: '',
Expand All @@ -76,6 +77,7 @@ export class RemoteClusterForm extends Component {
saveError: PropTypes.object,
fields: PropTypes.object,
disabledFields: PropTypes.object,
isNewCluster: PropTypes.bool,
};

static defaultProps = {
Expand All @@ -88,7 +90,7 @@ export class RemoteClusterForm extends Component {
constructor(props, context) {
super(props, context);

const { fields, disabledFields } = props;
const { fields, disabledFields, isNewCluster } = props;
const { isCloudEnabled } = context;

// Connection mode should default to "proxy" in cloud
Expand All @@ -104,6 +106,7 @@ export class RemoteClusterForm extends Component {
fieldsErrors: this.getFieldsErrors(fieldsState),
areErrorsVisible: false,
isRequestVisible: false,
isCloudFormVisible: isCloudEnabled && isNewCluster,
};
}

Expand All @@ -113,6 +116,12 @@ export class RemoteClusterForm extends Component {
}));
};

toggleIsCloudFormVisible = () => {
this.setState(({ isCloudFormVisible }) => ({
isCloudFormVisible: !isCloudFormVisible,
}));
};

getFieldsErrors(fields, seedInput = '') {
const { name, seeds, mode, proxyAddress, serverName } = fields;
const { isCloudEnabled } = this.context;
Expand Down Expand Up @@ -495,6 +504,15 @@ export class RemoteClusterForm extends Component {
id="xpack.remoteClusters.remoteClusterForm.sectionModeTitle"
defaultMessage="Connection mode"
/>
<EuiButtonEmpty size="s" onClick={this.toggleIsCloudFormVisible} iconType="logoCloud">
{/*
* TODO copy review
*/}
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.cloudFormButton"
defaultMessage="Add Cloud deployment"
/>
</EuiButtonEmpty>
</h2>
</EuiTitle>
}
Expand Down Expand Up @@ -878,6 +896,7 @@ export class RemoteClusterForm extends Component {

const {
isRequestVisible,
isCloudFormVisible,
areErrorsVisible,
fields: { name },
fieldsErrors: { name: errorClusterName },
Expand Down Expand Up @@ -949,13 +968,20 @@ export class RemoteClusterForm extends Component {

{this.renderSavingFeedback()}

{isRequestVisible ? (
{isRequestVisible && (
<RequestFlyout
name={name}
cluster={this.getAllFields()}
close={() => this.setState({ isRequestVisible: false })}
/>
) : null}
)}

{isCloudFormVisible && (
<CloudDeploymentForm
onClose={this.toggleIsCloudFormVisible}
onClusterConfigure={this.onFieldsChange}
/>
)}
</Fragment>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageContent } from '@elastic/eui';

import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { getRouter, redirect, setBreadcrumbs } from '../../services';
import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';

export class RemoteClusterAdd extends PureComponent {
Expand Down Expand Up @@ -79,6 +78,7 @@ export class RemoteClusterAdd extends PureComponent {
/>

<RemoteClusterForm
isNewCluster={true}
isSaving={isAddingCluster}
saveError={addClusterError}
save={this.save}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 {
validateCloudUrl,
convertCloudUrlToProxyConnection,
i18nTexts,
} from './cloud_deployment_url';
describe('Cloud deployment url', () => {
describe('validation', () => {
it('errors when the url is empty', () => {
const actual = validateCloudUrl('');
expect(actual).toBe(i18nTexts.urlInvalid);
});

it('errors when the url is invalid', () => {
const actual = validateCloudUrl('invalid%url');
expect(actual).toBe(i18nTexts.urlInvalid);
});
});

describe('conversion', () => {
it('empty url to empty proxy connection values', () => {
const actual = convertCloudUrlToProxyConnection('');
expect(actual).toEqual({ proxyAddress: '', serverName: '' });
});

it('url with protocol and port to proxy connection values', () => {
const actual = convertCloudUrlToProxyConnection('http://test.com:1234');
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
});

it('url with protocol and no port to proxy connection values', () => {
const actual = convertCloudUrlToProxyConnection('http://test.com');
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
});

it('url with no protocol to proxy connection values', () => {
const actual = convertCloudUrlToProxyConnection('test.com');
expect(actual).toEqual({ proxyAddress: 'test.com:9400', serverName: 'test.com' });
});
it('invalid url to empty proxy connection values', () => {
const actual = convertCloudUrlToProxyConnection('invalid%url');
expect(actual).toEqual({ proxyAddress: '', serverName: '' });
});
});
});
Loading