[Cloud] ES endpoint discovery (#167122)

This commit is contained in:
Sébastien Loix 2023-09-28 12:20:53 +01:00 committed by GitHub
parent b90b2114a0
commit 92a92fff67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 662 additions and 198 deletions

1
.github/CODEOWNERS vendored
View file

@ -67,6 +67,7 @@ packages/kbn-ci-stats-performance-metrics @elastic/kibana-operations
packages/kbn-ci-stats-reporter @elastic/kibana-operations
packages/kbn-ci-stats-shipper-cli @elastic/kibana-operations
packages/kbn-cli-dev-mode @elastic/kibana-operations
packages/cloud @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_chat @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_chat_provider @elastic/kibana-core
x-pack/plugins/cloud_integrations/cloud_data_migration @elastic/platform-onboarding

View file

@ -15,6 +15,7 @@
"customIntegrations": "src/plugins/custom_integrations",
"customIntegrationsPackage": "packages/kbn-custom-integrations",
"dashboard": "src/plugins/dashboard",
"cloud": "packages/cloud",
"domDragDrop": "packages/kbn-dom-drag-drop",
"controls": "src/plugins/controls",
"data": "src/plugins/data",

View file

@ -54,8 +54,9 @@ Details for each programming language library that Elastic provides are in the
https://www.elastic.co/guide/en/elasticsearch/client/index.html[{es} Client documentation].
If you are running {kib} on our hosted {es} Service,
click *View deployment details* on the *Integrations* view
click *Endpoints* on the *Integrations* view
to verify your {es} endpoint and Cloud ID, and create API keys for integration.
Alternatively, the *Endpoints* are also accessible through the top bar help menu.
[float]
=== Add sample data

View file

@ -173,6 +173,7 @@
"@kbn/chart-expressions-common": "link:src/plugins/chart_expressions/common",
"@kbn/chart-icons": "link:packages/kbn-chart-icons",
"@kbn/charts-plugin": "link:src/plugins/charts",
"@kbn/cloud": "link:packages/cloud",
"@kbn/cloud-chat-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat",
"@kbn/cloud-chat-provider-plugin": "link:x-pack/plugins/cloud_integrations/cloud_chat_provider",
"@kbn/cloud-data-migration-plugin": "link:x-pack/plugins/cloud_integrations/cloud_data_migration",

3
packages/cloud/README.md Normal file
View file

@ -0,0 +1,3 @@
# @kbn/cloud
Empty package generated by @kbn/generate

View file

@ -0,0 +1,81 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import {
EuiForm,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiButtonEmpty,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDeploymentDetails } from './services';
import { DeploymentDetailsEsInput } from './deployment_details_es_input';
import { DeploymentDetailsCloudIdInput } from './deployment_details_cloudid_input';
const hasActiveModifierKey = (event: React.MouseEvent): boolean => {
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
};
export const DeploymentDetails = ({ closeModal }: { closeModal?: () => void }) => {
const { cloudId, elasticsearchUrl, managementUrl, learnMoreUrl, navigateToUrl } =
useDeploymentDetails();
const isInsideModal = !!closeModal;
if (!cloudId) {
return null;
}
return (
<EuiForm component="div">
{/* Elastic endpoint */}
{elasticsearchUrl && <DeploymentDetailsEsInput elasticsearchUrl={elasticsearchUrl} />}
{/* Cloud ID */}
<DeploymentDetailsCloudIdInput cloudId={cloudId} />
<EuiSpacer size="m" />
{managementUrl && (
<EuiFlexGroup gutterSize="m" justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
href={managementUrl}
onClick={(e: React.MouseEvent) => {
if (!hasActiveModifierKey(e)) {
e.preventDefault();
navigateToUrl(managementUrl);
}
if (closeModal) {
closeModal();
}
}}
flush="left"
>
{i18n.translate('cloud.deploymentDetails.createManageApiKeysButtonLabel', {
defaultMessage: 'Create and manage API keys',
})}
</EuiButtonEmpty>
</EuiFlexItem>
{!isInsideModal && (
<EuiFlexItem grow={false}>
<EuiLink external href={learnMoreUrl} target="_blank">
{i18n.translate('cloud.deploymentDetails.learnMoreButtonLabel', {
defaultMessage: 'Learn more',
})}
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiForm>
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { type FC } from 'react';
import {
EuiFormRow,
EuiFieldText,
EuiCopy,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const DeploymentDetailsCloudIdInput: FC<{ cloudId: string }> = ({ cloudId }) => {
return (
<EuiFormRow
label={i18n.translate('cloud.deploymentDetails.cloudIDLabel', {
defaultMessage: 'Cloud ID',
})}
fullWidth
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldText
value={cloudId}
fullWidth
disabled
data-test-subj="deploymentDetailsCloudID"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={cloudId}>
{(copy) => (
<EuiButtonIcon onClick={copy} iconType="copyClipboard" display="base" size="m" />
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};

View file

@ -0,0 +1,48 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { type FC } from 'react';
import {
EuiFormRow,
EuiFieldText,
EuiCopy,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const DeploymentDetailsEsInput: FC<{ elasticsearchUrl: string }> = ({
elasticsearchUrl,
}) => {
return (
<EuiFormRow
label={i18n.translate('cloud.deploymentDetails.elasticEndpointLabel', {
defaultMessage: 'Elastic endpoint',
})}
fullWidth
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldText
value={elasticsearchUrl}
fullWidth
disabled
data-test-subj="deploymentDetailsEsEndpoint"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={elasticsearchUrl}>
{(copy) => (
<EuiButtonIcon onClick={copy} iconType="copyClipboard" display="base" size="m" />
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
};

View file

@ -0,0 +1,69 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { type FC } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
} from '@elastic/eui';
import { useDeploymentDetails } from './services';
import { DeploymentDetails } from './deployment_details';
interface Props {
closeModal: () => void;
}
export const DeploymentDetailsModal: FC<Props> = ({ closeModal }) => {
const { learnMoreUrl } = useDeploymentDetails();
return (
<EuiModal
onClose={() => {
closeModal();
}}
style={{ width: 600 }}
data-test-subj="deploymentDetailsModal"
>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('cloud.deploymentDetails.helpMenuLinks.endpoints', {
defaultMessage: 'Endpoints',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<DeploymentDetails closeModal={closeModal} />
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup alignItems="baseline" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiLink external href={learnMoreUrl} target="_blank">
{i18n.translate('cloud.deploymentDetails.modal.learnMoreButtonLabel', {
defaultMessage: 'Learn more',
})}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={closeModal} fill>
{i18n.translate('cloud.deploymentDetails.modal.closeButtonLabel', {
defaultMessage: 'Close',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { DeploymentDetailsKibanaProvider, DeploymentDetailsProvider } from './services';
export { DeploymentDetails } from './deployment_details';
export { DeploymentDetailsModal } from './deployment_details_modal';

View file

@ -0,0 +1,123 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC, useContext } from 'react';
export interface DeploymentDetailsContextValue {
cloudId?: string;
elasticsearchUrl?: string;
managementUrl?: string;
learnMoreUrl: string;
navigateToUrl(url: string): Promise<void>;
}
const DeploymentDetailsContext = React.createContext<DeploymentDetailsContextValue | null>(null);
/**
* Abstract external service Provider.
*/
export const DeploymentDetailsProvider: FC<DeploymentDetailsContextValue> = ({
children,
...services
}) => {
return (
<DeploymentDetailsContext.Provider value={services}>
{children}
</DeploymentDetailsContext.Provider>
);
};
/**
* Kibana-specific service types.
*/
export interface DeploymentDetailsKibanaDependencies {
/** CoreStart contract */
core: {
application: {
navigateToUrl(url: string): Promise<void>;
};
};
/** SharePluginStart contract */
share: {
url: {
locators: {
get(
id: string
): undefined | { useUrl: (params: { sectionId: string; appId: string }) => string };
};
};
};
/** CloudSetup contract */
cloud: {
isCloudEnabled: boolean;
cloudId?: string;
elasticsearchUrl?: string;
};
/** DocLinksStart contract */
docLinks: {
links: {
fleet: {
apiKeysLearnMore: string;
};
};
};
}
/**
* Kibana-specific Provider that maps to known dependency types.
*/
export const DeploymentDetailsKibanaProvider: FC<DeploymentDetailsKibanaDependencies> = ({
children,
...services
}) => {
const {
core: {
application: { navigateToUrl },
},
cloud: { isCloudEnabled, cloudId, elasticsearchUrl },
share: {
url: { locators },
},
docLinks: {
links: {
fleet: { apiKeysLearnMore },
},
},
} = services;
const managementUrl = locators
.get('MANAGEMENT_APP_LOCATOR')
?.useUrl({ sectionId: 'security', appId: 'api_keys' });
return (
<DeploymentDetailsProvider
cloudId={isCloudEnabled ? cloudId : undefined}
elasticsearchUrl={elasticsearchUrl}
managementUrl={managementUrl}
learnMoreUrl={apiKeysLearnMore}
navigateToUrl={navigateToUrl}
>
{children}
</DeploymentDetailsProvider>
);
};
/**
* React hook for accessing pre-wired services.
*/
export function useDeploymentDetails() {
const context = useContext(DeploymentDetailsContext);
if (!context) {
throw new Error(
'DeploymentDetailsContext is missing. Ensure your component or React root is wrapped with <DeploymentDetailsProvider /> or <DeploymentDetailsKibanaProvider />.'
);
}
return context;
}

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/cloud'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/cloud",
"owner": "@elastic/kibana-core"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/cloud",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
]
}

View file

@ -67,6 +67,7 @@ const buildDefaultContentLinks = ({
defaultMessage: 'Open an issue in GitHub',
}),
href: docLinks.links.kibana.createGithubIssue,
iconType: 'logoGithub',
},
];
@ -201,17 +202,40 @@ export class HeaderHelpMenu extends Component<Props, State> {
return (
<Fragment>
{defaultContentLinks.map(({ href, title, iconType }, i) => {
const isLast = i === defaultContentLinks.length - 1;
return (
<Fragment key={i}>
<EuiButtonEmpty href={href} target="_blank" size="s" flush="left" iconType={iconType}>
{title}
</EuiButtonEmpty>
{!isLast && <EuiSpacer size="xs" />}
</Fragment>
);
})}
{defaultContentLinks.map(
({ href, title, iconType, onClick: _onClick, dataTestSubj }, i) => {
const isLast = i === defaultContentLinks.length - 1;
if (href && _onClick) {
throw new Error(
'Only one of `href` and `onClick` should be provided for the help menu link.'
);
}
const hrefProps = href ? { href, target: '_blank' } : {};
const onClick = () => {
if (!_onClick) return;
_onClick();
this.closeMenu();
};
return (
<Fragment key={i}>
<EuiButtonEmpty
{...hrefProps}
onClick={onClick}
size="s"
flush="left"
iconType={iconType}
data-test-subj={dataTestSubj}
>
{title}
</EuiButtonEmpty>
{!isLast && <EuiSpacer size="xs" />}
</Fragment>
);
}
)}
</Fragment>
);
}

View file

@ -18,8 +18,10 @@ export interface ChromeNavControl {
/** @public */
export interface ChromeHelpMenuLink {
title: string;
href: string;
href?: string;
iconType?: string;
onClick?: () => void;
dataTestSubj?: string;
}
/**

View file

@ -128,6 +128,8 @@
"@kbn/ci-stats-shipper-cli/*": ["packages/kbn-ci-stats-shipper-cli/*"],
"@kbn/cli-dev-mode": ["packages/kbn-cli-dev-mode"],
"@kbn/cli-dev-mode/*": ["packages/kbn-cli-dev-mode/*"],
"@kbn/cloud": ["packages/cloud"],
"@kbn/cloud/*": ["packages/cloud/*"],
"@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"],
"@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"],
"@kbn/cloud-chat-provider-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat_provider"],

View file

@ -14,6 +14,9 @@
],
"requiredBundles": [
"kibanaReact"
],
"requiredPlugins": [
"share"
]
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 type { CoreStart } from '@kbn/core/public';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import {
DeploymentDetailsKibanaProvider,
DeploymentDetailsModal,
} from '@kbn/cloud/deployment_details';
interface Props {
closeModal: () => void;
core: CoreStart;
docLinks: DocLinksStart;
cloud: CloudStart;
share: SharePluginStart;
}
export const EndpointsModal = ({ core, share, cloud, docLinks, closeModal }: Props) => {
return (
<DeploymentDetailsKibanaProvider core={core} share={share} cloud={cloud} docLinks={docLinks}>
<DeploymentDetailsModal closeModal={closeModal} />
</DeploymentDetailsKibanaProvider>
);
};

View file

@ -4,17 +4,32 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { ChromeHelpMenuLink } from '@kbn/core-chrome-browser';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { CoreStart } from '@kbn/core/public';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { EndpointsModal } from './endpoints_modal';
export const createHelpMenuLinks = ({
docLinks,
helpSupportUrl,
core,
cloud,
share,
}: {
docLinks: DocLinksStart;
core: CoreStart;
cloud: CloudStart;
share: SharePluginStart;
helpSupportUrl: string;
}) => {
const { overlays } = core;
const helpMenuLinks: ChromeHelpMenuLink[] = [
{
title: i18n.translate('xpack.cloudLinks.helpMenuLinks.documentation', {
@ -34,6 +49,27 @@ export const createHelpMenuLinks = ({
}),
href: docLinks.links.kibana.feedback,
},
{
title: i18n.translate('xpack.cloudLinks.helpMenuLinks.endpoints', {
defaultMessage: 'Endpoints',
}),
iconType: 'console',
dataTestSubj: 'endpointsHelpLink',
onClick: () => {
const modal = overlays.openModal(
toMountPoint(
<EndpointsModal
core={core}
share={share}
cloud={cloud}
docLinks={docLinks}
closeModal={() => modal.close()}
/>,
{ theme: core.theme, i18n: core.i18n }
)
);
},
},
];
return helpMenuLinks;

View file

@ -8,6 +8,7 @@
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { coreMock } from '@kbn/core/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
import { maybeAddCloudLinks } from './maybe_add_cloud_links';
@ -18,6 +19,7 @@ describe('maybeAddCloudLinks', () => {
maybeAddCloudLinks({
core,
security,
share: sharePluginMock.createStartContract(),
cloud: { ...cloudMock.createStart(), isCloudEnabled: false },
});
// Since there's a promise, let's wait for the next tick
@ -35,6 +37,7 @@ describe('maybeAddCloudLinks', () => {
maybeAddCloudLinks({
security,
core,
share: sharePluginMock.createStartContract(),
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
});
// Since there's a promise, let's wait for the next tick
@ -90,6 +93,12 @@ describe('maybeAddCloudLinks', () => {
"href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback",
"title": "Give feedback",
},
Object {
"dataTestSubj": "endpointsHelpLink",
"iconType": "console",
"onClick": [Function],
"title": "Endpoints",
},
],
]
`);
@ -103,6 +112,7 @@ describe('maybeAddCloudLinks', () => {
maybeAddCloudLinks({
security,
core,
share: sharePluginMock.createStartContract(),
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
});
// Since there's a promise, let's wait for the next tick
@ -157,6 +167,12 @@ describe('maybeAddCloudLinks', () => {
"href": "https://www.elastic.co/products/kibana/feedback?blade=kibanafeedback",
"title": "Give feedback",
},
Object {
"dataTestSubj": "endpointsHelpLink",
"iconType": "console",
"onClick": [Function],
"title": "Endpoints",
},
],
]
`);
@ -172,6 +188,7 @@ describe('maybeAddCloudLinks', () => {
maybeAddCloudLinks({
security,
core,
share: sharePluginMock.createStartContract(),
cloud: { ...cloudMock.createStart(), isCloudEnabled: true },
});
// Since there's a promise, let's wait for the next tick

View file

@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { createUserMenuLinks } from './user_menu_links';
import { createHelpMenuLinks } from './help_menu_links';
@ -18,9 +19,10 @@ export interface MaybeAddCloudLinksDeps {
core: CoreStart;
security: SecurityPluginStart;
cloud: CloudStart;
share: SharePluginStart;
}
export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinksDeps): void {
export function maybeAddCloudLinks({ core, security, cloud, share }: MaybeAddCloudLinksDeps): void {
const userObservable = defer(() => security.authc.getCurrentUser()).pipe(
// Check if user is a cloud user.
map((user) => user.elastic_cloud_user),
@ -54,6 +56,9 @@ export function maybeAddCloudLinks({ core, security, cloud }: MaybeAddCloudLinks
const helpMenuLinks = createHelpMenuLinks({
docLinks: core.docLinks,
helpSupportUrl,
core,
share,
cloud,
});
core.chrome.setHelpMenuLinks(helpMenuLinks);

View file

@ -11,6 +11,7 @@ import { coreMock } from '@kbn/core/public/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
describe('Cloud Links Plugin - public', () => {
let plugin: CloudLinksPlugin;
@ -40,7 +41,11 @@ describe('Cloud Links Plugin - public', () => {
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
plugin.start(coreStart, { cloud, guidedOnboarding });
plugin.start(coreStart, {
cloud,
guidedOnboarding,
share: sharePluginMock.createStartContract(),
});
expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).toHaveBeenCalledTimes(1);
});
@ -48,14 +53,22 @@ describe('Cloud Links Plugin - public', () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
plugin.start(coreStart, { cloud, guidedOnboarding });
plugin.start(coreStart, {
cloud,
guidedOnboarding,
share: sharePluginMock.createStartContract(),
});
expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled();
});
test('does not register the Onboarding Setup Guide link when cloud is not enabled', () => {
const coreStart = coreMock.createStart();
const cloud = { ...cloudMock.createStart(), isCloudEnabled: false };
plugin.start(coreStart, { cloud, guidedOnboarding });
plugin.start(coreStart, {
cloud,
guidedOnboarding,
share: sharePluginMock.createStartContract(),
});
expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled();
});
});
@ -72,7 +85,11 @@ describe('Cloud Links Plugin - public', () => {
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
plugin.start(coreStart, { cloud, guidedOnboarding });
plugin.start(coreStart, {
cloud,
guidedOnboarding,
share: sharePluginMock.createStartContract(),
});
expect(coreStart.chrome.registerGlobalHelpExtensionMenuLink).not.toHaveBeenCalled();
});
});
@ -83,7 +100,7 @@ describe('Cloud Links Plugin - public', () => {
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
const security = securityMock.createStart();
plugin.start(coreStart, { cloud, security });
plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() });
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(1);
});
@ -91,7 +108,7 @@ describe('Cloud Links Plugin - public', () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
plugin.start(coreStart, { cloud });
plugin.start(coreStart, { cloud, share: sharePluginMock.createStartContract() });
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
});
@ -100,7 +117,7 @@ describe('Cloud Links Plugin - public', () => {
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: true };
const security = securityMock.createStart();
plugin.start(coreStart, { cloud, security });
plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() });
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
});
@ -108,7 +125,7 @@ describe('Cloud Links Plugin - public', () => {
const coreStart = coreMock.createStart();
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const security = securityMock.createStart();
plugin.start(coreStart, { security });
plugin.start(coreStart, { security, share: sharePluginMock.createStartContract() });
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
});
@ -117,7 +134,7 @@ describe('Cloud Links Plugin - public', () => {
coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false);
const cloud = { ...cloudMock.createStart(), isCloudEnabled: false };
const security = securityMock.createStart();
plugin.start(coreStart, { cloud, security });
plugin.start(coreStart, { cloud, security, share: sharePluginMock.createStartContract() });
expect(maybeAddCloudLinksMock).toHaveBeenCalledTimes(0);
});
});

View file

@ -11,6 +11,7 @@ import type { CoreStart, Plugin } from '@kbn/core/public';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { maybeAddCloudLinks } from './maybe_add_cloud_links';
interface CloudLinksDepsSetup {
@ -21,6 +22,7 @@ interface CloudLinksDepsSetup {
interface CloudLinksDepsStart {
cloud?: CloudStart;
security?: SecurityPluginStart;
share: SharePluginStart;
guidedOnboarding?: GuidedOnboardingPluginStart;
}
@ -29,7 +31,7 @@ export class CloudLinksPlugin
{
public setup() {}
public start(core: CoreStart, { cloud, security, guidedOnboarding }: CloudLinksDepsStart) {
public start(core: CoreStart, { cloud, security, guidedOnboarding, share }: CloudLinksDepsStart) {
if (cloud?.isCloudEnabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname)) {
if (guidedOnboarding?.guidedOnboardingApi?.isEnabled) {
core.chrome.registerGlobalHelpExtensionMenuLink({
@ -42,11 +44,13 @@ export class CloudLinksPlugin
priority: 1000, // We want this link to be at the very top.
});
}
if (security) {
maybeAddCloudLinks({
core,
security,
cloud,
share,
});
}
}

View file

@ -23,6 +23,9 @@
"@kbn/user-profile-components",
"@kbn/core-lifecycle-browser",
"@kbn/kibana-react-plugin",
"@kbn/share-plugin",
"@kbn/cloud",
"@kbn/react-kibana-mount",
],
"exclude": [
"target/**/*",

View file

@ -1,102 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import styled from 'styled-components';
import {
EuiPopover,
EuiText,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiCopy,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiLink,
EuiHeaderLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export interface Props {
cloudId: string;
managementUrl?: string;
learnMoreUrl: string;
}
const Description = styled(EuiText)`
margin-bottom: ${({ theme }) => theme.eui.euiSizeL};
`;
export const DeploymentDetails = ({ cloudId, learnMoreUrl, managementUrl }: Props) => {
const [isOpen, setIsOpen] = React.useState(false);
const button = (
<EuiHeaderLink onClick={() => setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive>
{i18n.translate('xpack.fleet.integrations.deploymentButton', {
defaultMessage: 'View deployment details',
})}
</EuiHeaderLink>
);
const management = managementUrl ? (
<EuiFormRow label="API keys" fullWidth>
<EuiFlexGroup gutterSize="m" alignItems="center">
<EuiFlexItem>
<EuiButton href={managementUrl}>Create and manage API keys</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiLink external href={learnMoreUrl} target="_blank">
Learn more
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
) : null;
return (
<EuiPopover
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
button={button}
anchorPosition="downCenter"
>
<div style={{ width: 450 }}>
<Description>
{i18n.translate('xpack.fleet.integrations.deploymentDescription', {
defaultMessage:
'Send data to Elastic from your applications by referencing your deployment.',
})}
</Description>
<EuiForm component="div">
<EuiFormRow label="Cloud ID" fullWidth>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldText value={cloudId} fullWidth disabled />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={cloudId}>
{(copy) => (
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
display="base"
size="m"
/>
)}
</EuiCopy>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
{management}
</EuiForm>
</div>
</EuiPopover>
);
};

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Meta } from '@storybook/react';
import { EuiHeader } from '@elastic/eui';
import { DeploymentDetails as ConnectedComponent } from './deployment_details';
import type { Props as PureComponentProps } from './deployment_details.component';
import { DeploymentDetails as PureComponent } from './deployment_details.component';
export default {
title: 'Sections/EPM/Deployment Details',
description: '',
decorators: [
(storyFn) => {
const sections = [{ items: [] }, { items: [storyFn()] }];
return <EuiHeader sections={sections} />;
},
],
} as Meta;
export const DeploymentDetails = () => {
return <ConnectedComponent />;
};
DeploymentDetails.args = {
isCloudEnabled: true,
};
DeploymentDetails.argTypes = {
isCloudEnabled: {
type: {
name: 'boolean',
},
defaultValue: true,
control: {
type: 'boolean',
},
},
};
export const Component = (props: PureComponentProps) => {
return <PureComponent {...props} />;
};
Component.args = {
cloudId: 'cloud-id',
learnMoreUrl: 'https://learn-more-url',
managementUrl: 'https://management-url',
};

View file

@ -6,13 +6,18 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiPopover, EuiHeaderLink } from '@elastic/eui';
import {
DeploymentDetailsKibanaProvider,
DeploymentDetails as DeploymentDetailsComponent,
} from '@kbn/cloud/deployment_details';
import { useStartServices } from '../../hooks';
import { DeploymentDetails as Component } from './deployment_details.component';
export const DeploymentDetails = () => {
const { share, cloud, docLinks } = useStartServices();
const [isOpen, setIsOpen] = React.useState(false);
const { share, cloud, docLinks, application } = useStartServices();
// If the cloud plugin isn't enabled, we can't display the flyout.
if (!cloud) {
@ -21,16 +26,36 @@ export const DeploymentDetails = () => {
const { isCloudEnabled, cloudId } = cloud;
// If cloud isn't enabled or we don't have a cloudId we can't display the flyout.
// If cloud isn't enabled or we don't have a cloudId we don't render the button.
if (!isCloudEnabled || !cloudId) {
return null;
}
const managementUrl = share.url.locators
.get('MANAGEMENT_APP_LOCATOR')
?.useUrl({ sectionId: 'security', appId: 'api_keys' });
const button = (
<EuiHeaderLink onClick={() => setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive>
{i18n.translate('xpack.fleet.integrations.endpointsButton', {
defaultMessage: 'Endpoints',
})}
</EuiHeaderLink>
);
const learnMoreUrl = docLinks.links.fleet.apiKeysLearnMore;
return <Component {...{ cloudId, managementUrl, learnMoreUrl }} />;
return (
<DeploymentDetailsKibanaProvider
core={{ application }}
share={share}
cloud={cloud}
docLinks={docLinks}
>
<EuiPopover
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
button={button}
anchorPosition="downCenter"
>
<div style={{ width: 450 }}>
<DeploymentDetailsComponent />
</div>
</EuiPopover>
</DeploymentDetailsKibanaProvider>
);
};

View file

@ -101,5 +101,6 @@
"@kbn/core-saved-objects-base-server-internal",
"@kbn/core-http-common",
"@kbn/dashboard-plugin",
"@kbn/cloud",
]
}

View file

@ -16425,8 +16425,6 @@
"xpack.fleet.homeIntegration.tutorialModule.noticeText.notePrefix": "Note",
"xpack.fleet.initializationErrorMessageTitle": "Initialisation de Fleet impossible",
"xpack.fleet.integrations.customInputsLink": "entrées personnalisées",
"xpack.fleet.integrations.deploymentButton": "Voir les détails du déploiement",
"xpack.fleet.integrations.deploymentDescription": "Envoyez des données à Elastic à partir de vos applications en référençant votre déploiement.",
"xpack.fleet.integrations.discussForumLink": "forum",
"xpack.fleet.integrations.installPackage.uploadedTooltip": "Cette intégration a été installée par le biais d'un chargement et ne peut pas être réinstallée automatiquement. Veuillez la charger à nouveau pour la réinstaller.",
"xpack.fleet.integrations.integrationSaved": "Paramètres de l'intégration enregistrés",

View file

@ -16439,8 +16439,6 @@
"xpack.fleet.homeIntegration.tutorialModule.noticeText.notePrefix": "注",
"xpack.fleet.initializationErrorMessageTitle": "Fleet を初期化できません",
"xpack.fleet.integrations.customInputsLink": "カスタム入力",
"xpack.fleet.integrations.deploymentButton": "デプロイ詳細の表示",
"xpack.fleet.integrations.deploymentDescription": "デプロイを参照し、アプリケーションのデータをElasticに送信します。",
"xpack.fleet.integrations.discussForumLink": "フォーラム",
"xpack.fleet.integrations.installPackage.uploadedTooltip": "この統合はアップロードによってインストールされたため、自動的に再インストールできません。再インストールするには、もう一度アップロードしてください。",
"xpack.fleet.integrations.integrationSaved": "統合設定が保存されました",

View file

@ -16439,8 +16439,6 @@
"xpack.fleet.homeIntegration.tutorialModule.noticeText.notePrefix": "备注",
"xpack.fleet.initializationErrorMessageTitle": "无法初始化 Fleet",
"xpack.fleet.integrations.customInputsLink": "定制输入",
"xpack.fleet.integrations.deploymentButton": "查看部署详情",
"xpack.fleet.integrations.deploymentDescription": "通过引用部署,将数据从应用程序发送到 Elastic。",
"xpack.fleet.integrations.discussForumLink": "论坛",
"xpack.fleet.integrations.installPackage.uploadedTooltip": "此集成通过上传进行安装,因此无法自动重新安装。请再次将其上传,以便重新安装。",
"xpack.fleet.integrations.integrationSaved": "已保存集成设置",

View file

@ -44,7 +44,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${samlIdPPlugin}`,
'--xpack.cloud.id=ftr_fake_cloud_id',
// Note: the base64 string in the cloud.id config contains the ES endpoint required in the functional tests
'--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=',
'--xpack.cloud.base_url=https://cloud.elastic.co',
'--xpack.cloud.deployment_url=/deployments/deploymentId',
'--xpack.cloud.organization_url=/organization/organizationId',

View file

@ -46,6 +46,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await find.byCssSelector('[data-test-subj="cloudOnboardingSetupGuideLink"]')
).to.not.be(null);
});
it('A button to open a modal to view the CloudID and ES endpoint is added', async () => {
await PageObjects.common.clickAndValidate('helpMenuButton', 'endpointsHelpLink');
expect(await find.byCssSelector('[data-test-subj="endpointsHelpLink"]')).to.not.be(null);
// Open the modal
await PageObjects.common.clickAndValidate('endpointsHelpLink', 'deploymentDetailsModal');
const esEndpointInput = await find.byCssSelector(
'[data-test-subj="deploymentDetailsEsEndpoint"]'
);
const esEndpointValue = await esEndpointInput.getAttribute('value');
expect(esEndpointValue).to.be('https://ES123abc.hello.com:443');
const cloudIdInput = await find.byCssSelector(
'[data-test-subj="deploymentDetailsCloudID"]'
);
const cloudIdInputValue = await cloudIdInput.getAttribute('value');
expect(cloudIdInputValue).to.be(
'ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM='
);
});
});
it('"Manage this deployment" is appended to the nav list', async () => {

View file

@ -3243,6 +3243,10 @@
version "0.0.0"
uid ""
"@kbn/cloud@link:packages/cloud":
version "0.0.0"
uid ""
"@kbn/code-editor-mocks@link:packages/shared-ux/code_editor/mocks":
version "0.0.0"
uid ""