diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index be401c26dc0d2..89f769c0f12ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.i18nrc.json b/.i18nrc.json index b5e17c18d3542..4657840019f6c 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -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", diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index e271eb6cce5c0..fef9ae71a085b 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -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 diff --git a/package.json b/package.json index 3b7f8c030fc8f..313ee86a8171d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/cloud/README.md b/packages/cloud/README.md new file mode 100644 index 0000000000000..e387c4b9be959 --- /dev/null +++ b/packages/cloud/README.md @@ -0,0 +1,3 @@ +# @kbn/cloud + +Empty package generated by @kbn/generate diff --git a/packages/cloud/deployment_details/deployment_details.tsx b/packages/cloud/deployment_details/deployment_details.tsx new file mode 100644 index 0000000000000..278709f7b6d32 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details.tsx @@ -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 ( + + {/* Elastic endpoint */} + {elasticsearchUrl && } + + {/* Cloud ID */} + + + + + {managementUrl && ( + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + if (!hasActiveModifierKey(e)) { + e.preventDefault(); + navigateToUrl(managementUrl); + } + if (closeModal) { + closeModal(); + } + }} + flush="left" + > + {i18n.translate('cloud.deploymentDetails.createManageApiKeysButtonLabel', { + defaultMessage: 'Create and manage API keys', + })} + + + {!isInsideModal && ( + + + {i18n.translate('cloud.deploymentDetails.learnMoreButtonLabel', { + defaultMessage: 'Learn more', + })} + + + )} + + )} + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx new file mode 100644 index 0000000000000..a749fe4371715 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_cloudid_input.tsx @@ -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 ( + + + + + + + + {(copy) => ( + + )} + + + + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_es_input.tsx b/packages/cloud/deployment_details/deployment_details_es_input.tsx new file mode 100644 index 0000000000000..2998b5bade543 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_es_input.tsx @@ -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 ( + + + + + + + + {(copy) => ( + + )} + + + + + ); +}; diff --git a/packages/cloud/deployment_details/deployment_details_modal.tsx b/packages/cloud/deployment_details/deployment_details_modal.tsx new file mode 100644 index 0000000000000..2f3d628c2ca47 --- /dev/null +++ b/packages/cloud/deployment_details/deployment_details_modal.tsx @@ -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 = ({ closeModal }) => { + const { learnMoreUrl } = useDeploymentDetails(); + + return ( + { + closeModal(); + }} + style={{ width: 600 }} + data-test-subj="deploymentDetailsModal" + > + + + {i18n.translate('cloud.deploymentDetails.helpMenuLinks.endpoints', { + defaultMessage: 'Endpoints', + })} + + + + + + + + + + {i18n.translate('cloud.deploymentDetails.modal.learnMoreButtonLabel', { + defaultMessage: 'Learn more', + })} + + + + + {i18n.translate('cloud.deploymentDetails.modal.closeButtonLabel', { + defaultMessage: 'Close', + })} + + + + + + ); +}; diff --git a/packages/cloud/deployment_details/index.ts b/packages/cloud/deployment_details/index.ts new file mode 100644 index 0000000000000..2f37291eecd7c --- /dev/null +++ b/packages/cloud/deployment_details/index.ts @@ -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'; diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx new file mode 100644 index 0000000000000..c4e8be12bb547 --- /dev/null +++ b/packages/cloud/deployment_details/services.tsx @@ -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; +} + +const DeploymentDetailsContext = React.createContext(null); + +/** + * Abstract external service Provider. + */ +export const DeploymentDetailsProvider: FC = ({ + children, + ...services +}) => { + return ( + + {children} + + ); +}; + +/** + * Kibana-specific service types. + */ +export interface DeploymentDetailsKibanaDependencies { + /** CoreStart contract */ + core: { + application: { + navigateToUrl(url: string): Promise; + }; + }; + /** 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 = ({ + 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 ( + + {children} + + ); +}; + +/** + * 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 or .' + ); + } + + return context; +} diff --git a/packages/cloud/jest.config.js b/packages/cloud/jest.config.js new file mode 100644 index 0000000000000..174f01cfc1be6 --- /dev/null +++ b/packages/cloud/jest.config.js @@ -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: ['/packages/cloud'], +}; diff --git a/packages/cloud/kibana.jsonc b/packages/cloud/kibana.jsonc new file mode 100644 index 0000000000000..e39a0dbe40617 --- /dev/null +++ b/packages/cloud/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/cloud", + "owner": "@elastic/kibana-core" +} diff --git a/packages/cloud/package.json b/packages/cloud/package.json new file mode 100644 index 0000000000000..8e0023dc5c7a3 --- /dev/null +++ b/packages/cloud/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/cloud", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/cloud/tsconfig.json b/packages/cloud/tsconfig.json new file mode 100644 index 0000000000000..c4703bc51cf6c --- /dev/null +++ b/packages/cloud/tsconfig.json @@ -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", + ] +} diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx index e1e43d43ab401..5c3d5bb048737 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/header/header_help_menu.tsx @@ -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 { return ( - {defaultContentLinks.map(({ href, title, iconType }, i) => { - const isLast = i === defaultContentLinks.length - 1; - return ( - - - {title} - - {!isLast && } - - ); - })} + {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 ( + + + {title} + + {!isLast && } + + ); + } + )} ); } diff --git a/packages/core/chrome/core-chrome-browser/src/nav_controls.ts b/packages/core/chrome/core-chrome-browser/src/nav_controls.ts index 39b5d1b3b59b1..22c074862151b 100644 --- a/packages/core/chrome/core-chrome-browser/src/nav_controls.ts +++ b/packages/core/chrome/core-chrome-browser/src/nav_controls.ts @@ -18,8 +18,10 @@ export interface ChromeNavControl { /** @public */ export interface ChromeHelpMenuLink { title: string; - href: string; + href?: string; iconType?: string; + onClick?: () => void; + dataTestSubj?: string; } /** diff --git a/tsconfig.base.json b/tsconfig.base.json index 030b5c9bbed4c..bb2aec9d5819f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"], diff --git a/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc b/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc index 4b6625f842f79..660f6e64a2446 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc +++ b/x-pack/plugins/cloud_integrations/cloud_links/kibana.jsonc @@ -14,6 +14,9 @@ ], "requiredBundles": [ "kibanaReact" + ], + "requiredPlugins": [ + "share" ] } } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/endpoints_modal.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/endpoints_modal.tsx new file mode 100644 index 0000000000000..7c6b23d352f1a --- /dev/null +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/endpoints_modal.tsx @@ -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 ( + + + + ); +}; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx similarity index 53% rename from x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts rename to x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx index 82b0e86e6569a..15270c5876214 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/help_menu_links.tsx @@ -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( + modal.close()} + />, + { theme: core.theme, i18n: core.i18n } + ) + ); + }, + }, ]; return helpMenuLinks; diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts index b9045fdc9a59f..d680d6cce4f4f 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.test.ts @@ -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 diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts index 33fb4df7bfce2..2772c87d124d3 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/maybe_add_cloud_links/maybe_add_cloud_links.ts @@ -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); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts index d928b7a6f0e8a..d2f987337a440 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.test.ts @@ -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); }); }); diff --git a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx index 38b568791b70b..bfebe531276d4 100755 --- a/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx +++ b/x-pack/plugins/cloud_integrations/cloud_links/public/plugin.tsx @@ -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, }); } } diff --git a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json index f1a67895cdd5e..43f411cadf060 100644 --- a/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json +++ b/x-pack/plugins/cloud_integrations/cloud_links/tsconfig.json @@ -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/**/*", diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx deleted file mode 100644 index 7e3b414cf0f6d..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.component.tsx +++ /dev/null @@ -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 = ( - setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive> - {i18n.translate('xpack.fleet.integrations.deploymentButton', { - defaultMessage: 'View deployment details', - })} - - ); - - const management = managementUrl ? ( - - - - Create and manage API keys - - - - Learn more - - - - - ) : null; - - return ( - setIsOpen(false)} - button={button} - anchorPosition="downCenter" - > -
- - {i18n.translate('xpack.fleet.integrations.deploymentDescription', { - defaultMessage: - 'Send data to Elastic from your applications by referencing your deployment.', - })} - - - - - - - - - - {(copy) => ( - - )} - - - - - {management} - -
-
- ); -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx deleted file mode 100644 index 5b311b3443e36..0000000000000 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.stories.tsx +++ /dev/null @@ -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 ; - }, - ], -} as Meta; - -export const DeploymentDetails = () => { - return ; -}; - -DeploymentDetails.args = { - isCloudEnabled: true, -}; - -DeploymentDetails.argTypes = { - isCloudEnabled: { - type: { - name: 'boolean', - }, - defaultValue: true, - control: { - type: 'boolean', - }, - }, -}; - -export const Component = (props: PureComponentProps) => { - return ; -}; - -Component.args = { - cloudId: 'cloud-id', - learnMoreUrl: 'https://learn-more-url', - managementUrl: 'https://management-url', -}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx index 968d596a5f9d5..ea17bc3f201c4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/components/header/deployment_details.tsx @@ -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 learnMoreUrl = docLinks.links.fleet.apiKeysLearnMore; - - return ; + const button = ( + setIsOpen(!isOpen)} iconType="iInCircle" iconSide="left" isActive> + {i18n.translate('xpack.fleet.integrations.endpointsButton', { + defaultMessage: 'Endpoints', + })} + + ); + + return ( + + setIsOpen(false)} + button={button} + anchorPosition="downCenter" + > +
+ +
+
+
+ ); }; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index b3f8a96417f9a..58cdfa25d1e08 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -101,5 +101,6 @@ "@kbn/core-saved-objects-base-server-internal", "@kbn/core-http-common", "@kbn/dashboard-plugin", + "@kbn/cloud", ] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b468f00d02466..4ae31ffb358ef 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7f59456c6bd14..87e23ff29cb1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "統合設定が保存されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8474d0289c239..6e3c128ec642a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "已保存集成设置", diff --git a/x-pack/test/functional_cloud/config.ts b/x-pack/test/functional_cloud/config.ts index c3203677631a9..df75e83138ed5 100644 --- a/x-pack/test/functional_cloud/config.ts +++ b/x-pack/test/functional_cloud/config.ts @@ -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', diff --git a/x-pack/test/functional_cloud/tests/cloud_links.ts b/x-pack/test/functional_cloud/tests/cloud_links.ts index 873cd943ec59d..94f67401c98fd 100644 --- a/x-pack/test/functional_cloud/tests/cloud_links.ts +++ b/x-pack/test/functional_cloud/tests/cloud_links.ts @@ -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 () => { diff --git a/yarn.lock b/yarn.lock index 41769ceb9a6ef..9c52f8dd23ae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 ""