diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index b16e75abdb8a1..3796076d0a3cd 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -43,6 +43,7 @@ const STORYBOOKS = [ 'observability', 'presentation', 'security_solution', + 'serverless', 'shared_ux', 'triggers_actions_ui', 'ui_actions_enhanced', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 93f49f1277ac7..d76ed31b2b2b1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -572,6 +572,10 @@ packages/kbn-securitysolution-t-grid @elastic/security-solution-platform packages/kbn-securitysolution-utils @elastic/security-solution-platform packages/kbn-server-http-tools @elastic/kibana-core packages/kbn-server-route-repository @elastic/apm-ui +x-pack/plugins/serverless @elastic/appex-sharedux +packages/serverless/project_switcher @elastic/appex-sharedux +packages/serverless/storybook/config @elastic/appex-sharedux +packages/serverless/types @elastic/appex-sharedux test/plugin_functional/plugins/session_notifications @elastic/kibana-core x-pack/plugins/session_view @elastic/sec-cloudnative-integrations packages/kbn-set-map @elastic/kibana-operations diff --git a/.i18nrc.json b/.i18nrc.json index 31ab7a91e206c..c2bf494c68c06 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -83,6 +83,7 @@ "share": "src/plugins/share", "sharedUXPackages": "packages/shared-ux", "securitySolutionPackages": "x-pack/packages/security-solution", + "serverlessPackages": "packages/serverless", "coloring": "packages/kbn-coloring/src", "languageDocumentationPopover": "packages/kbn-language-documentation-popover/src", "statusPage": "src/legacy/core_plugins/status_page", diff --git a/config/serverless.es.yml b/config/serverless.es.yml index e69de29bb2d1d..71b4f03446401 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -0,0 +1 @@ +xpack.serverless.plugin.developer.projectSwitcher.currentType: 'search' diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index ba76648238348..ddf1066edb882 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -1 +1,2 @@ xpack.infra.logs.app_target: discover +xpack.serverless.plugin.developer.projectSwitcher.currentType: 'observability' diff --git a/config/serverless.security.yml b/config/serverless.security.yml index e69de29bb2d1d..efa3558e0e9d9 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -0,0 +1 @@ +xpack.serverless.plugin.developer.projectSwitcher.currentType: 'security' diff --git a/config/serverless.yml b/config/serverless.yml index e65b15f064328..ec24139422975 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -1,4 +1,6 @@ newsfeed.enabled: false +xpack.security.showNavLinks: false +xpack.serverless.plugin.enabled: true xpack.fleet.enableExperimental: ['fleetServerStandalone'] xpack.fleet.internal.disableILMPolicies: true diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6b534f45b4f2d..412a9e8b5569e 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -706,6 +706,10 @@ Kibana. |Welcome to the Kibana Security Solution plugin! This README will go over getting started with development and testing. +|{kib-repo}blob/{branch}/x-pack/plugins/serverless/README.mdx[serverless] +| + + |{kib-repo}blob/{branch}/x-pack/plugins/session_view/README.md[sessionView] |Session View is meant to provide a visualization into what is going on in a particular Linux environment where the agent is running. It looks likes a terminal emulator; however, it is a tool for introspecting process activity and understanding user and service behaviour in your Linux servers and infrastructure. It is a time-ordered series of process executions displayed in a tree over time. diff --git a/package.json b/package.json index e6761869fa3c7..0751d8d84ea09 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "lint:es": "node scripts/eslint", "lint:style": "node scripts/stylelint", "makelogs": "node scripts/makelogs", + "serverless": "node scripts/kibana --dev --serverless", "serverless-es": "node scripts/kibana --dev --serverless=es", "serverless-oblt": "node scripts/kibana --dev --serverless=oblt", "serverless-security": "node scripts/kibana --dev --serverless=security", @@ -573,6 +574,9 @@ "@kbn/securitysolution-utils": "link:packages/kbn-securitysolution-utils", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", + "@kbn/serverless": "link:x-pack/plugins/serverless", + "@kbn/serverless-project-switcher": "link:packages/serverless/project_switcher", + "@kbn/serverless-types": "link:packages/serverless/types", "@kbn/session-notifications-plugin": "link:test/plugin_functional/plugins/session_notifications", "@kbn/session-view-plugin": "link:x-pack/plugins/session_view", "@kbn/set-map": "link:packages/kbn-set-map", @@ -1112,6 +1116,7 @@ "@kbn/repo-source-classifier": "link:packages/kbn-repo-source-classifier", "@kbn/repo-source-classifier-cli": "link:packages/kbn-repo-source-classifier-cli", "@kbn/security-api-integration-helpers": "link:x-pack/test/security_api_integration/packages/helpers", + "@kbn/serverless-storybook-config": "link:packages/serverless/storybook/config", "@kbn/some-dev-log": "link:packages/kbn-some-dev-log", "@kbn/sort-package-json": "link:packages/kbn-sort-package-json", "@kbn/spec-to-console": "link:packages/kbn-spec-to-console", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts index 37b1b9a2eab7d..0087c5d019f98 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.ts @@ -126,6 +126,7 @@ describe('start', () => { Array [ Array [ "kbnBody", + "kbnBody--classicLayout", "kbnBody--noHeaderBanner", "kbnBody--chromeHidden", "kbnVersion-1-2-3", @@ -143,6 +144,7 @@ describe('start', () => { Array [ Array [ "kbnBody", + "kbnBody--classicLayout", "kbnBody--noHeaderBanner", "kbnBody--chromeHidden", "kbnVersion-8-0-0", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx index 4e0762aee8620..41362b3d80dcd 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx +++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx @@ -26,6 +26,7 @@ import type { ChromeGlobalHelpExtensionMenuLink, ChromeHelpExtension, ChromeUserBanner, + ChromeStyle, } from '@kbn/core-chrome-browser'; import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; @@ -33,7 +34,7 @@ import { DocTitleService } from './doc_title'; import { NavControlsService } from './nav_controls'; import { NavLinksService } from './nav_links'; import { RecentlyAccessedService } from './recently_accessed'; -import { Header } from './ui'; +import { Header, ProjectHeader } from './ui'; import type { InternalChromeStart } from './types'; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -119,6 +120,7 @@ export class ChromeService { const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const chromeStyle$ = new BehaviorSubject('classic'); const getKbnVersionClass = () => { // we assume that the version is valid and has the form 'X.X.X' @@ -131,10 +133,11 @@ export class ChromeService { }; const headerBanner$ = new BehaviorSubject(undefined); - const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe( - map(([headerBanner, isVisible]) => { + const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!, chromeStyle$]).pipe( + map(([headerBanner, isVisible, chromeStyle]) => { return [ 'kbnBody', + chromeStyle === 'project' ? 'kbnBody--projectLayout' : 'kbnBody--classicLayout', headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner', isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden', getKbnVersionClass(), @@ -163,6 +166,10 @@ export class ChromeService { const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$)); + const setChromeStyle = (style: ChromeStyle) => { + chromeStyle$.next(style); + }; + const isIE = () => { const ua = window.navigator.userAgent; const msie = ua.indexOf('MSIE '); // IE 10 or older @@ -203,41 +210,65 @@ export class ChromeService { }); } + const getHeaderComponent = () => { + const Component = ({ style$ }: { style$: typeof chromeStyle$ }) => { + if (style$.getValue() === 'project') { + return ( + + ); + } + + return ( +
+ ); + }; + return ; + }; + return { navControls, navLinks, recentlyAccessed, docTitle, - - getHeaderComponent: () => ( -
- ), + getHeaderComponent, getIsVisible$: () => this.isVisible$, @@ -302,6 +333,8 @@ export class ChromeService { }, getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)), + setChromeStyle, + getChromeStyle$: () => chromeStyle$.pipe(takeUntil(this.stop$)), }; } diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts b/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts index 5afd3e0f587bb..7a5ecadd26f23 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/index.ts @@ -7,5 +7,6 @@ */ export { Header } from './header'; +export { ProjectHeader } from './project'; export { LoadingIndicator } from './loading_indicator'; export type { NavType } from './header'; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx new file mode 100644 index 0000000000000..e85ae262c3bb7 --- /dev/null +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/header.tsx @@ -0,0 +1,90 @@ +/* + * 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 { Router } from 'react-router-dom'; +import { EuiHeader, EuiHeaderLogo, EuiHeaderSection, EuiHeaderSectionItem } from '@elastic/eui'; +import { + ChromeBreadcrumb, + ChromeGlobalHelpExtensionMenuLink, + ChromeHelpExtension, + ChromeNavControl, +} from '@kbn/core-chrome-browser/src'; +import { Observable } from 'rxjs'; +import { MountPoint } from '@kbn/core-mount-utils-browser'; +import { InternalApplicationStart } from '@kbn/core-application-browser-internal'; +import { HeaderBreadcrumbs } from '../header/header_breadcrumbs'; +import { HeaderActionMenu } from '../header/header_action_menu'; +import { HeaderHelpMenu } from '../header/header_help_menu'; +import { HeaderNavControls } from '../header/header_nav_controls'; +import { ProjectNavigation } from './navigation'; + +interface Props { + breadcrumbs$: Observable; + actionMenu$: Observable; + kibanaDocLink: string; + globalHelpExtensionMenuLinks$: Observable; + helpExtension$: Observable; + helpSupportUrl$: Observable; + kibanaVersion: string; + application: InternalApplicationStart; + navControlsRight$: Observable; +} + +export const ProjectHeader = ({ + application, + kibanaDocLink, + kibanaVersion, + ...observables +}: Props) => { + const renderLogo = () => ( + e.preventDefault()} + aria-label="Go to home page" + /> + ); + + return ( + <> + + + {renderLogo()} + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts new file mode 100644 index 0000000000000..af18e057731b0 --- /dev/null +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ProjectHeader } from './header'; diff --git a/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx new file mode 100644 index 0000000000000..20549325ec851 --- /dev/null +++ b/packages/core/chrome/core-chrome-browser-internal/src/ui/project/navigation.tsx @@ -0,0 +1,76 @@ +/* + * 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, { useCallback } from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { css } from '@emotion/react'; + +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiCollapsibleNav, EuiThemeProvider, useEuiTheme } from '@elastic/eui'; + +const LOCAL_STORAGE_IS_OPEN_KEY = 'PROJECT_NAVIGATION_OPEN' as const; +const SIZE_OPEN = 248; +const SIZE_CLOSED = 40; + +const buttonCSS = css` + margin-left: -32px; + margin-top: 12px; + position: fixed; + z-index: 1000; +`; + +const openAriaLabel = i18n.translate('core.ui.chrome.projectNav.collapsibleNavOpenAriaLabel', { + defaultMessage: 'Close navigation', +}); + +const closedAriaLabel = i18n.translate('core.ui.chrome.projectNav.collapsibleNavClosedAriaLabel', { + defaultMessage: 'Open navigation', +}); + +export const ProjectNavigation: React.FC = ({ children }) => { + const { euiTheme, colorMode } = useEuiTheme(); + + const [isOpen, setIsOpen] = useLocalStorage(LOCAL_STORAGE_IS_OPEN_KEY, true); + + const toggleOpen = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen, setIsOpen]); + + const collabsibleNavCSS = css` + border-inline-end-width: 1, + background: ${euiTheme.colors.darkestShade}, + display: flex, + flex-direction: row, + `; + + return ( + + + + + } + > + {isOpen && children} + + + ); +}; diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json index 4d4d6cad3bc21..cd27209bef12c 100644 --- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json +++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json @@ -5,7 +5,10 @@ "types": [ "jest", "node", - "react" + "react", + "@kbn/ambient-ui-types", + "@kbn/ambient-storybook-types", + "@emotion/react/types/css-prop" ] }, "include": [ diff --git a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts index 2f5c4deb1f38d..c7c62c7811277 100644 --- a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts +++ b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts @@ -61,6 +61,8 @@ const createStartContractMock = () => { setHeaderBanner: jest.fn(), hasHeaderBanner$: jest.fn(), getBodyClasses$: jest.fn(), + getChromeStyle$: jest.fn(), + setChromeStyle: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getIsVisible$.mockReturnValue(new BehaviorSubject(false)); diff --git a/packages/core/chrome/core-chrome-browser/index.ts b/packages/core/chrome/core-chrome-browser/index.ts index 3fbef34126a4a..1d2dca4c957bc 100644 --- a/packages/core/chrome/core-chrome-browser/index.ts +++ b/packages/core/chrome/core-chrome-browser/index.ts @@ -7,25 +7,26 @@ */ export type { - ChromeUserBanner, + ChromeBadge, ChromeBreadcrumb, + ChromeBreadcrumbsAppendExtension, + ChromeDocTitle, + ChromeGlobalHelpExtensionMenuLink, ChromeHelpExtension, - ChromeHelpExtensionMenuLink, ChromeHelpExtensionLinkBase, + ChromeHelpExtensionMenuCustomLink, + ChromeHelpExtensionMenuDiscussLink, + ChromeHelpExtensionMenuDocumentationLink, + ChromeHelpExtensionMenuGitHubLink, + ChromeHelpExtensionMenuLink, ChromeHelpMenuActions, - ChromeNavLink, - ChromeBreadcrumbsAppendExtension, - ChromeNavLinks, ChromeNavControl, ChromeNavControls, - ChromeBadge, - ChromeHelpExtensionMenuGitHubLink, - ChromeHelpExtensionMenuDocumentationLink, - ChromeHelpExtensionMenuDiscussLink, - ChromeHelpExtensionMenuCustomLink, - ChromeGlobalHelpExtensionMenuLink, - ChromeDocTitle, - ChromeStart, + ChromeNavLink, + ChromeNavLinks, ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, + ChromeStart, + ChromeStyle, + ChromeUserBanner, } from './src'; diff --git a/packages/core/chrome/core-chrome-browser/src/contracts.ts b/packages/core/chrome/core-chrome-browser/src/contracts.ts index a81d9c3c6338f..3f6f756e2d2b1 100644 --- a/packages/core/chrome/core-chrome-browser/src/contracts.ts +++ b/packages/core/chrome/core-chrome-browser/src/contracts.ts @@ -13,7 +13,7 @@ import type { ChromeDocTitle } from './doc_title'; import type { ChromeNavControls } from './nav_controls'; import type { ChromeHelpExtension } from './help_extension'; import type { ChromeBreadcrumb, ChromeBreadcrumbsAppendExtension } from './breadcrumb'; -import type { ChromeBadge, ChromeUserBanner } from './types'; +import type { ChromeBadge, ChromeStyle, ChromeUserBanner } from './types'; import { ChromeGlobalHelpExtensionMenuLink } from './help_extension'; /** @@ -150,4 +150,15 @@ export interface ChromeStart { * Get an observable of the current header banner presence state. */ hasHeaderBanner$(): Observable; + + /** + * Sets the style type of the chrome. + * @param style The style type to apply to the chrome. + */ + setChromeStyle(style: ChromeStyle): void; + + /** + * Get an observable of the current style type of the chrome. + */ + getChromeStyle$(): Observable; } diff --git a/packages/core/chrome/core-chrome-browser/src/index.ts b/packages/core/chrome/core-chrome-browser/src/index.ts index 716af097fded7..89ba12d616d0e 100644 --- a/packages/core/chrome/core-chrome-browser/src/index.ts +++ b/packages/core/chrome/core-chrome-browser/src/index.ts @@ -26,4 +26,4 @@ export type { ChromeRecentlyAccessed, ChromeRecentlyAccessedHistoryItem, } from './recently_accessed'; -export type { ChromeBadge, ChromeUserBanner } from './types'; +export type { ChromeBadge, ChromeUserBanner, ChromeStyle } from './types'; diff --git a/packages/core/chrome/core-chrome-browser/src/types.ts b/packages/core/chrome/core-chrome-browser/src/types.ts index 81b8c32a1a04c..d4374687ff828 100644 --- a/packages/core/chrome/core-chrome-browser/src/types.ts +++ b/packages/core/chrome/core-chrome-browser/src/types.ts @@ -20,3 +20,6 @@ export interface ChromeBadge { export interface ChromeUserBanner { content: MountPoint; } + +/** @public */ +export type ChromeStyle = 'classic' | 'project'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 56259881447db..af6396c11e063 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -115,6 +115,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 65433 securitySolution: 66738 + serverless: 16573 sessionView: 77750 share: 71239 snapshotRestore: 79032 diff --git a/packages/serverless/project_switcher/README.mdx b/packages/serverless/project_switcher/README.mdx new file mode 100644 index 0000000000000..240988346458c --- /dev/null +++ b/packages/serverless/project_switcher/README.mdx @@ -0,0 +1,12 @@ +--- +id: serverless/components/ProjectSwitcher +slug: /serverless/components/project-switcher +title: Project Switcher +description: A popup which allows a developer to switch between project types on their dev server. +tags: ['serverless', 'component'] +date: 2023-04-23 +--- + +When working on Serverless instances of Kibana, developers likely want to switch between different project types to test changes. This Project Switcher is intended to be placed into the header bar by the Serverless plugin when the server is in development mode to allow "quick switching" between configurations. + +The connected component uses `http` to post a selection to a given API endpoint, intended to alter the YML configuration and trigger Watcher to restart the server. To that end, it will post its message to a given API endpoint and replace the content of `document.body`. The remainder of the process is left to the Serverless plugin. diff --git a/packages/serverless/project_switcher/index.ts b/packages/serverless/project_switcher/index.ts new file mode 100644 index 0000000000000..69148308099a5 --- /dev/null +++ b/packages/serverless/project_switcher/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 type { ProjectSwitcherProps, KibanaDependencies } from './src'; + +export { ProjectSwitcher, ProjectSwitcherKibanaProvider, ProjectSwitcherProvider } from './src'; diff --git a/packages/serverless/project_switcher/jest.config.js b/packages/serverless/project_switcher/jest.config.js new file mode 100644 index 0000000000000..713bdaaedaca2 --- /dev/null +++ b/packages/serverless/project_switcher/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/serverless/project_switcher'], +}; diff --git a/packages/serverless/project_switcher/kibana.jsonc b/packages/serverless/project_switcher/kibana.jsonc new file mode 100644 index 0000000000000..6e37bb95cafda --- /dev/null +++ b/packages/serverless/project_switcher/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/serverless-project-switcher", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/serverless/project_switcher/mocks/jest.mock.ts b/packages/serverless/project_switcher/mocks/jest.mock.ts new file mode 100644 index 0000000000000..935b89b63dd13 --- /dev/null +++ b/packages/serverless/project_switcher/mocks/jest.mock.ts @@ -0,0 +1,23 @@ +/* + * 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 { Services, KibanaDependencies } from '../src/types'; + +export const getProjectSwitcherServicesMock: () => jest.Mocked = () => ({ + setProjectType: jest.fn(), +}); + +export const getProjectSwitcherKibanaDependenciesMock: () => jest.Mocked = + () => ({ + coreStart: { + http: { + post: jest.fn(() => Promise.resolve({ data: {} })), + }, + }, + projectChangeAPIUrl: 'serverless/change_project', + }); diff --git a/packages/serverless/project_switcher/mocks/storybook.mock.ts b/packages/serverless/project_switcher/mocks/storybook.mock.ts new file mode 100644 index 0000000000000..08faa007c5eb5 --- /dev/null +++ b/packages/serverless/project_switcher/mocks/storybook.mock.ts @@ -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 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 { action } from '@storybook/addon-actions'; +import { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock'; + +import type { ProjectSwitcherProps, Services } from '../src/types'; + +type PropArguments = Pick; + +/** + * Storybook parameters provided from the controls addon. + */ +export type ProjectSwitcherStorybookParams = Record; + +/** + * Storybook mocks for the `NoDataCard` component. + */ +export class ProjectSwitcherStorybookMock extends AbstractStorybookMock< + ProjectSwitcherProps, + Services, + PropArguments, + {} +> { + propArguments = { + currentProjectType: { + control: { type: 'radio' }, + options: ['observability', 'security', 'search'], + defaultValue: 'observability', + }, + }; + serviceArguments = {}; + dependencies = []; + + getProps(params?: ProjectSwitcherStorybookParams): ProjectSwitcherProps { + return { + currentProjectType: this.getArgumentValue('currentProjectType', params), + }; + } + + getServices(_params: ProjectSwitcherStorybookParams): Services { + return { + setProjectType: action('setProjectType'), + }; + } +} diff --git a/packages/serverless/project_switcher/package.json b/packages/serverless/project_switcher/package.json new file mode 100644 index 0000000000000..5910a823783d5 --- /dev/null +++ b/packages/serverless/project_switcher/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/serverless-project-switcher", + "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/serverless/project_switcher/src/constants.ts b/packages/serverless/project_switcher/src/constants.ts new file mode 100644 index 0000000000000..e3a277bfc7953 --- /dev/null +++ b/packages/serverless/project_switcher/src/constants.ts @@ -0,0 +1,24 @@ +/* + * 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 type { IconType } from '@elastic/eui'; +import type { ProjectType } from '@kbn/serverless-types'; + +export const icons: Record = { + observability: 'logoObservability', + security: 'logoSecurity', + search: 'logoEnterpriseSearch', +} as const; + +export const labels: Record = { + observability: 'Observability', + security: 'Security', + search: 'Enterprise Search', +} as const; + +export const projectTypes: ProjectType[] = ['security', 'observability', 'search']; diff --git a/packages/serverless/project_switcher/src/header_button.tsx b/packages/serverless/project_switcher/src/header_button.tsx new file mode 100644 index 0000000000000..ee1bd0acc5888 --- /dev/null +++ b/packages/serverless/project_switcher/src/header_button.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 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, { MouseEventHandler } from 'react'; +import { EuiHeaderSectionItemButton, EuiIcon } from '@elastic/eui'; + +import { ProjectType } from '@kbn/serverless-types'; + +import { icons } from './constants'; + +export const TEST_ID = 'projectSwitcherButton'; + +export interface Props { + onClick: MouseEventHandler; + currentProjectType: ProjectType; +} + +export const HeaderButton = ({ onClick, currentProjectType }: Props) => ( + + + +); diff --git a/packages/serverless/project_switcher/src/index.ts b/packages/serverless/project_switcher/src/index.ts new file mode 100644 index 0000000000000..adb6beef6514f --- /dev/null +++ b/packages/serverless/project_switcher/src/index.ts @@ -0,0 +1,12 @@ +/* + * 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 type { KibanaDependencies, ProjectSwitcherProps } from './types'; + +export { ProjectSwitcher } from './switcher'; +export { ProjectSwitcherKibanaProvider, ProjectSwitcherProvider } from './services'; diff --git a/packages/serverless/project_switcher/src/item.tsx b/packages/serverless/project_switcher/src/item.tsx new file mode 100644 index 0000000000000..71bca220c450a --- /dev/null +++ b/packages/serverless/project_switcher/src/item.tsx @@ -0,0 +1,33 @@ +/* + * 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 { EuiIcon, EuiKeyPadMenuItem, type EuiIconProps } from '@elastic/eui'; +import { ProjectType } from '@kbn/serverless-types'; + +import { labels, icons } from './constants'; + +type OnChangeType = (id: string, value?: any) => void; + +interface ItemProps extends Pick { + type: ProjectType; + onChange: (type: ProjectType) => void; + isSelected: boolean; +} + +export const SwitcherItem = ({ type: id, onChange, isSelected }: ItemProps) => ( + + + +); diff --git a/packages/serverless/project_switcher/src/loader.tsx b/packages/serverless/project_switcher/src/loader.tsx new file mode 100644 index 0000000000000..854f6e6d9f61b --- /dev/null +++ b/packages/serverless/project_switcher/src/loader.tsx @@ -0,0 +1,21 @@ +/* + * 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 { Logo, type Props } from './logo'; + +export const Loader = (props: Props) => ( +
+
+ +
Loading Project
+
+
+
+); diff --git a/packages/serverless/project_switcher/src/logo.tsx b/packages/serverless/project_switcher/src/logo.tsx new file mode 100644 index 0000000000000..e0d827b18f4f2 --- /dev/null +++ b/packages/serverless/project_switcher/src/logo.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import type { ProjectType } from '@kbn/serverless-types'; + +export interface Props { + project: ProjectType; +} + +export const Logo = ({ project }: Props) => { + let type = 'logoElastic'; + switch (project) { + case 'search': + type = 'logoElasticsearch'; + break; + case 'security': + type = 'logoSecurity'; + break; + case 'observability': + type = 'logoObservability'; + break; + } + + return ; +}; diff --git a/packages/serverless/project_switcher/src/services.tsx b/packages/serverless/project_switcher/src/services.tsx new file mode 100644 index 0000000000000..413de92d4f1ad --- /dev/null +++ b/packages/serverless/project_switcher/src/services.tsx @@ -0,0 +1,63 @@ +/* + * 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'; +import ReactDOM from 'react-dom'; +import { Loader } from './loader'; + +import type { Services, KibanaDependencies } from './types'; + +const Context = React.createContext(null); + +/** + * A Context Provider that provides services to the component and its dependencies. + */ +export const ProjectSwitcherProvider: FC = ({ children, ...services }) => { + return {children}; +}; + +/** + * Kibana-specific Provider that maps dependencies to services. + */ +export const ProjectSwitcherKibanaProvider: FC = ({ + children, + coreStart, + projectChangeAPIUrl, +}) => { + const value: Services = { + setProjectType: (projectType) => { + coreStart.http + .post(projectChangeAPIUrl, { body: JSON.stringify({ id: projectType }) }) + .then(() => { + ReactDOM.render(, document.body); + + // Give the watcher a couple of seconds to see the file change. + setTimeout(() => { + window.location.href = '/'; + }, 2000); + }); + }, + }; + + return {children}; +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useServices() { + const context = useContext(Context); + + if (!context) { + throw new Error( + 'ProjectSwitcher Context is missing. Ensure your component or React root is wrapped with ProjectSwitcherContext.' + ); + } + + return context; +} diff --git a/packages/serverless/project_switcher/src/switcher.component.tsx b/packages/serverless/project_switcher/src/switcher.component.tsx new file mode 100644 index 0000000000000..fd319f3839d6d --- /dev/null +++ b/packages/serverless/project_switcher/src/switcher.component.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useState } from 'react'; +import { css } from '@emotion/react'; +import { EuiPopover, useGeneratedHtmlId, EuiPopoverTitle, EuiKeyPadMenu } from '@elastic/eui'; + +import { ProjectType } from '@kbn/serverless-types'; + +import { SwitcherItem } from './item'; +import type { ProjectSwitcherComponentProps } from './types'; +import { HeaderButton } from './header_button'; +import { projectTypes } from './constants'; + +export { TEST_ID as TEST_ID_BUTTON } from './header_button'; +export const TEST_ID_ITEM_GROUP = 'projectSwitcherItemGroup'; + +const switcherCSS = css` + min-width: 240px; +`; + +export const ProjectSwitcher = ({ + currentProjectType, + onProjectChange, +}: ProjectSwitcherComponentProps) => { + const [isOpen, setIsOpen] = useState(false); + const id = useGeneratedHtmlId({ + prefix: 'switcherPopover', + }); + + const closePopover = () => { + setIsOpen(false); + }; + + const onButtonClick = () => { + setIsOpen(!isOpen); + }; + + const onChange = (projectType: ProjectType) => { + closePopover(); + onProjectChange(projectType); + return false; + }; + + const items = projectTypes.map((type) => ( + + )); + + const button = ; + + return ( + + Switch Project Type + + {items} + + + ); +}; diff --git a/packages/serverless/project_switcher/src/switcher.stories.tsx b/packages/serverless/project_switcher/src/switcher.stories.tsx new file mode 100644 index 0000000000000..09bece7b00f27 --- /dev/null +++ b/packages/serverless/project_switcher/src/switcher.stories.tsx @@ -0,0 +1,42 @@ +/* + * 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 { + ProjectSwitcherStorybookMock, + type ProjectSwitcherStorybookParams, +} from '../mocks/storybook.mock'; + +import { ProjectSwitcher as Component } from './switcher'; +import { ProjectSwitcherProvider as Provider } from './services'; + +import mdx from '../README.mdx'; + +export default { + title: 'Developer/Project Switcher', + description: '', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +const mock = new ProjectSwitcherStorybookMock(); +const argTypes = mock.getArgumentTypes(); + +export const ProjectSwitcher = (params: ProjectSwitcherStorybookParams) => { + return ( + + + + ); +}; + +ProjectSwitcher.argTypes = argTypes; diff --git a/packages/serverless/project_switcher/src/switcher.test.tsx b/packages/serverless/project_switcher/src/switcher.test.tsx new file mode 100644 index 0000000000000..5c4fac1d8c161 --- /dev/null +++ b/packages/serverless/project_switcher/src/switcher.test.tsx @@ -0,0 +1,151 @@ +/* + * 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 { render, RenderResult, screen, within, waitFor, cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ProjectType } from '@kbn/serverless-types'; + +import { ProjectSwitcherKibanaProvider, ProjectSwitcherProvider } from './services'; +import { + getProjectSwitcherKibanaDependenciesMock, + getProjectSwitcherServicesMock, +} from '../mocks/jest.mock'; +import { ProjectSwitcher } from './switcher'; +import { + ProjectSwitcher as ProjectSwitcherComponent, + TEST_ID_BUTTON, + TEST_ID_ITEM_GROUP, +} from './switcher.component'; +import { KibanaDependencies, Services } from './types'; + +const renderKibanaProjectSwitcher = ( + currentProjectType: ProjectType = 'observability' +): [RenderResult, jest.Mocked] => { + const mock = getProjectSwitcherKibanaDependenciesMock(); + return [ + render( + + + + ), + mock, + ]; +}; + +const renderProjectSwitcher = ( + currentProjectType: ProjectType = 'observability' +): [RenderResult, jest.Mocked] => { + const mock = getProjectSwitcherServicesMock(); + return [ + render( + + + + ), + mock, + ]; +}; + +describe('ProjectSwitcher', () => { + describe('Component', () => { + test('is rendered', () => { + expect(() => + render( + + ) + ).not.toThrowError(); + }); + }); + + describe('Connected Component', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + test("doesn't render if the Provider is missing", () => { + expect(() => render()).toThrowError(); + }); + + describe('with Services', () => { + test('is rendered', () => { + renderProjectSwitcher(); + const button = screen.queryByTestId(TEST_ID_BUTTON); + expect(button).not.toBeNull(); + }); + + test('opens', async () => { + renderProjectSwitcher(); + + let group = screen.queryByTestId(TEST_ID_ITEM_GROUP); + expect(group).toBeNull(); + + const button = screen.getByTestId(TEST_ID_BUTTON); + await waitFor(() => userEvent.click(button)); + + group = screen.queryByTestId(TEST_ID_ITEM_GROUP); + expect(group).not.toBeNull(); + }); + + test('calls setProjectType when clicked', async () => { + const [_, mock] = renderProjectSwitcher(); + + const button = screen.getByTestId(TEST_ID_BUTTON); + await waitFor(() => userEvent.click(button)); + + const group = screen.getByTestId(TEST_ID_ITEM_GROUP); + const project = await within(group).findByLabelText('Security'); + await waitFor(() => userEvent.click(project)); + + expect(mock.setProjectType).toHaveBeenCalled(); + }); + }); + }); + + describe('with Kibana Dependencies', () => { + beforeEach(() => { + cleanup(); + }); + + test('is rendered', () => { + renderKibanaProjectSwitcher(); + const button = screen.queryByTestId(TEST_ID_BUTTON); + expect(button).not.toBeNull(); + }); + + test('opens', async () => { + renderKibanaProjectSwitcher(); + + let group = screen.queryByTestId(TEST_ID_ITEM_GROUP); + expect(group).toBeNull(); + + const button = screen.getByTestId(TEST_ID_BUTTON); + userEvent.click(button); + + group = screen.queryByTestId(TEST_ID_ITEM_GROUP); + expect(group).not.toBeNull(); + }); + + test('posts message to change project', async () => { + const [_, mock] = renderKibanaProjectSwitcher(); + + const button = screen.getByTestId(TEST_ID_BUTTON); + await waitFor(() => userEvent.click(button)); + + const group = screen.getByTestId(TEST_ID_ITEM_GROUP); + const project = await within(group).findByLabelText('Security'); + await waitFor(() => userEvent.click(project)); + + expect(mock.coreStart.http.post).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/serverless/project_switcher/src/switcher.tsx b/packages/serverless/project_switcher/src/switcher.tsx new file mode 100644 index 0000000000000..4fdacf31987b6 --- /dev/null +++ b/packages/serverless/project_switcher/src/switcher.tsx @@ -0,0 +1,21 @@ +/* + * 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 { ProjectType } from '@kbn/serverless-types'; +import { ProjectSwitcher as Component } from './switcher.component'; + +import { useServices } from './services'; +import type { ProjectSwitcherProps } from './types'; + +export const ProjectSwitcher = (props: ProjectSwitcherProps) => { + const { setProjectType } = useServices(); + const onProjectChange = (projectType: ProjectType) => setProjectType(projectType); + + return ; +}; diff --git a/packages/serverless/project_switcher/src/types.ts b/packages/serverless/project_switcher/src/types.ts new file mode 100644 index 0000000000000..5f0b8fcf55c15 --- /dev/null +++ b/packages/serverless/project_switcher/src/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { ProjectType } from '@kbn/serverless-types'; + +/** + * A list of services that are consumed by this component. + */ +export interface Services { + setProjectType: (projectType: ProjectType) => void; +} + +/** + * An interface containing a collection of Kibana plugins and services required to + * render this component. + */ +export interface KibanaDependencies { + coreStart: { + http: { + post: (path: string, options: { body: string }) => Promise; + }; + }; + projectChangeAPIUrl: string; +} + +/** + * Props for the `ProjectSwitcher` pure component. + */ +export interface ProjectSwitcherComponentProps { + onProjectChange: (projectType: ProjectType) => void; + currentProjectType: ProjectType; +} + +export type ProjectSwitcherProps = Pick; diff --git a/packages/serverless/project_switcher/tsconfig.json b/packages/serverless/project_switcher/tsconfig.json new file mode 100644 index 0000000000000..8fd6b54236754 --- /dev/null +++ b/packages/serverless/project_switcher/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/shared-ux-storybook-mock", + "@kbn/serverless-types", + ] +} diff --git a/packages/serverless/storybook/config/README.mdx b/packages/serverless/storybook/config/README.mdx new file mode 100644 index 0000000000000..bba4efa56f3ae --- /dev/null +++ b/packages/serverless/storybook/config/README.mdx @@ -0,0 +1,5 @@ +# Serverless Storybook config + +This directory contains the configuration for the Storybook deployment for all Serverless component packages. + +For more information, refer to the [Storybook documentation](https://storybook.js.org/docs/react/configure/overview) and the `@kbn/storybook` package. diff --git a/packages/serverless/storybook/config/constants.ts b/packages/serverless/storybook/config/constants.ts new file mode 100644 index 0000000000000..be7ae8926447f --- /dev/null +++ b/packages/serverless/storybook/config/constants.ts @@ -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. + */ + +/** The title of the Storybook. */ +export const TITLE = 'Serverless Storybook'; + +/** The remote URL of the root from which Storybook loads stories for Serverless. */ +export const URL = 'https://github.com/elastic/kibana/tree/main/packages/serverless'; diff --git a/packages/serverless/storybook/config/index.ts b/packages/serverless/storybook/config/index.ts new file mode 100755 index 0000000000000..5a73da614bf27 --- /dev/null +++ b/packages/serverless/storybook/config/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { TITLE, URL } from './constants'; diff --git a/packages/serverless/storybook/config/kibana.jsonc b/packages/serverless/storybook/config/kibana.jsonc new file mode 100644 index 0000000000000..a141e67afd745 --- /dev/null +++ b/packages/serverless/storybook/config/kibana.jsonc @@ -0,0 +1,6 @@ +{ + "type": "shared-common", + "id": "@kbn/serverless-storybook-config", + "owner": "@elastic/appex-sharedux", + "devOnly": true +} diff --git a/packages/serverless/storybook/config/main.ts b/packages/serverless/storybook/config/main.ts new file mode 100644 index 0000000000000..47a47a5a802b3 --- /dev/null +++ b/packages/serverless/storybook/config/main.ts @@ -0,0 +1,17 @@ +/* + * 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 { defaultConfig } from '@kbn/storybook'; + +module.exports = { + ...defaultConfig, + stories: ['../../**/*.stories.+(tsx|mdx)'], + reactOptions: { + strictMode: true, + }, +}; diff --git a/packages/serverless/storybook/config/manager.ts b/packages/serverless/storybook/config/manager.ts new file mode 100644 index 0000000000000..fb973258b9053 --- /dev/null +++ b/packages/serverless/storybook/config/manager.ts @@ -0,0 +1,23 @@ +/* + * 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 { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID as selectedPanel } from '@storybook/addon-actions'; + +import { TITLE as brandTitle, URL as brandUrl } from './constants'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle, + brandUrl, + }), + selectedPanel, + showPanel: true.valueOf, +}); diff --git a/packages/serverless/storybook/config/package.json b/packages/serverless/storybook/config/package.json new file mode 100644 index 0000000000000..3ff7ce7258729 --- /dev/null +++ b/packages/serverless/storybook/config/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/serverless-storybook-config", + "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/serverless/storybook/config/preview.ts b/packages/serverless/storybook/config/preview.ts new file mode 100644 index 0000000000000..ee65b88614fb9 --- /dev/null +++ b/packages/serverless/storybook/config/preview.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-namespace,@typescript-eslint/no-empty-interface */ +declare global { + namespace NodeJS { + interface Global {} + interface InspectOptions {} + type ConsoleConstructor = console.ConsoleConstructor; + } +} + +/* eslint-enable */ +import jest from 'jest-mock'; + +/* @ts-expect-error TS doesn't see jest as a property of window, and I don't want to edit our global config. */ +window.jest = jest; diff --git a/packages/serverless/storybook/config/tsconfig.json b/packages/serverless/storybook/config/tsconfig.json new file mode 100644 index 0000000000000..1d676d9c2948d --- /dev/null +++ b/packages/serverless/storybook/config/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/storybook", + ] +} diff --git a/packages/serverless/types/README.mdx b/packages/serverless/types/README.mdx new file mode 100644 index 0000000000000..b12bc55becfc0 --- /dev/null +++ b/packages/serverless/types/README.mdx @@ -0,0 +1,10 @@ +--- +id: serverless/packages/types +slug: /serverless/packages/types +title: Serverless Typescript Types +description: A package of common types for Serverless projects. +tags: ['serverless', 'package'] +date: 2023-04-23 +--- + +This package contains common types for Serverless projects. \ No newline at end of file diff --git a/packages/serverless/types/index.d.ts b/packages/serverless/types/index.d.ts new file mode 100644 index 0000000000000..b384e7a59fbba --- /dev/null +++ b/packages/serverless/types/index.d.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 type ProjectType = 'observability' | 'security' | 'search'; diff --git a/packages/serverless/types/kibana.jsonc b/packages/serverless/types/kibana.jsonc new file mode 100644 index 0000000000000..0b5a8fffe84be --- /dev/null +++ b/packages/serverless/types/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/serverless-types", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/serverless/types/package.json b/packages/serverless/types/package.json new file mode 100644 index 0000000000000..c9b7b0810fdfb --- /dev/null +++ b/packages/serverless/types/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/serverless-types", + "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/serverless/types/tsconfig.json b/packages/serverless/types/tsconfig.json new file mode 100644 index 0000000000000..6d27b06d5f8ba --- /dev/null +++ b/packages/serverless/types/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 9facf94408235..46f4de867ed58 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -8,7 +8,7 @@ import { set as lodashSet } from '@kbn/safer-lodash-set'; import _ from 'lodash'; -import { statSync } from 'fs'; +import { statSync, copyFileSync, existsSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; @@ -29,7 +29,7 @@ function getServerlessProjectMode(opts) { return null; } - if (VALID_SERVERLESS_PROJECT_MODE.includes(opts.serverless)) { + if (VALID_SERVERLESS_PROJECT_MODE.includes(opts.serverless) || opts.serverless === true) { return opts.serverless; } @@ -115,6 +115,38 @@ function maybeAddConfig(name, configs, method) { } } +/** + * @param {string} file + * @param {'es' | 'security' | 'oblt' | true} mode + * @param {string[]} configs + * @param {'push' | 'unshift'} method + */ +function maybeSetRecentConfig(file, mode, configs, method) { + const path = resolve(getConfigDirectory(), file); + + try { + if (mode === true) { + if (!existsSync(path)) { + const data = readFileSync(path.replace('recent', 'es'), 'utf-8'); + writeFileSync( + path, + `${data}\nxpack.serverless.plugin.developer.projectSwitcher.enabled: true\n` + ); + } + } else { + copyFileSync(path.replace('recent', mode), path); + } + + configs[method](path); + } catch (err) { + if (err.code === 'ENOENT') { + return; + } + + throw err; + } +} + /** * @returns {string[]} */ @@ -255,7 +287,15 @@ export default function (program) { } if (isServerlessCapableDistribution()) { - command.option('--serverless ', 'Start Kibana in a serverless project mode'); + command + .option( + '--serverless', + 'Start Kibana in the most recent serverless project mode, (default is es)' + ) + .option( + '--serverless ', + 'Start Kibana in a specific serverless project mode' + ); } if (DEV_MODE_SUPPORTED) { @@ -285,7 +325,7 @@ export default function (program) { // we "unshift" .serverless. config so that it only overrides defaults if (serverlessMode) { maybeAddConfig(`serverless.yml`, configs, 'push'); - maybeAddConfig(`serverless.${serverlessMode}.yml`, configs, 'unshift'); + maybeSetRecentConfig('serverless.recent.yml', serverlessMode, configs, 'unshift'); } // .dev. configs are "pushed" so that they override all other config files @@ -293,7 +333,7 @@ export default function (program) { maybeAddConfig('kibana.dev.yml', configs, 'push'); if (serverlessMode) { maybeAddConfig(`serverless.dev.yml`, configs, 'push'); - maybeAddConfig(`serverless.${serverlessMode}.dev.yml`, configs, 'push'); + maybeSetRecentConfig('serverless.recent.dev.yml', serverlessMode, configs, 'unshift'); } } diff --git a/src/core/public/styles/rendering/_base.scss b/src/core/public/styles/rendering/_base.scss index 9d4296ca3b4ef..a9ece9955e6ca 100644 --- a/src/core/public/styles/rendering/_base.scss +++ b/src/core/public/styles/rendering/_base.scss @@ -75,6 +75,9 @@ &.kbnBody--chromeHidden { @include kbnAffordForHeader(0); } + &.kbnBody--projectLayout { + @include kbnAffordForHeader($euiHeaderHeightCompensation); + } &.kbnBody--chromeHidden.kbnBody--hasHeaderBanner { @include kbnAffordForHeader($kbnHeaderBannerHeight); } diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 24dc93c33894e..e050926610259 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -46,6 +46,7 @@ export const storybookAliases = { presentation: 'src/plugins/presentation_util/storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', security_solution_packages: 'x-pack/packages/security-solution/storybook/config', + serverless: 'packages/serverless/storybook/config', shared_ux: 'packages/shared-ux/storybook/config', threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook', triggers_actions_ui: 'x-pack/plugins/triggers_actions_ui/.storybook', diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 7d624e7d1c94f..a4591f6f5f47e 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -223,6 +223,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.security.loginAssistanceMessage (string)', 'xpack.security.sameSiteCookies (alternatives)', 'xpack.security.showInsecureClusterWarning (boolean)', + 'xpack.security.showNavLinks (boolean)', 'xpack.securitySolution.enableExperimental (array)', 'xpack.securitySolution.prebuiltRulesPackageVersion (string)', 'xpack.snapshot_restore.slm_ui.enabled (boolean)', @@ -270,6 +271,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.security.loginAssistanceMessage (string)', 'xpack.security.sameSiteCookies (alternatives)', 'xpack.security.showInsecureClusterWarning (boolean)', + 'xpack.security.showNavLinks (boolean)', ]; // We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large // arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's diff --git a/tsconfig.base.json b/tsconfig.base.json index a5de10e9f4fe3..f333d6f58b34f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1138,6 +1138,14 @@ "@kbn/server-http-tools/*": ["packages/kbn-server-http-tools/*"], "@kbn/server-route-repository": ["packages/kbn-server-route-repository"], "@kbn/server-route-repository/*": ["packages/kbn-server-route-repository/*"], + "@kbn/serverless": ["x-pack/plugins/serverless"], + "@kbn/serverless/*": ["x-pack/plugins/serverless/*"], + "@kbn/serverless-project-switcher": ["packages/serverless/project_switcher"], + "@kbn/serverless-project-switcher/*": ["packages/serverless/project_switcher/*"], + "@kbn/serverless-storybook-config": ["packages/serverless/storybook/config"], + "@kbn/serverless-storybook-config/*": ["packages/serverless/storybook/config/*"], + "@kbn/serverless-types": ["packages/serverless/types"], + "@kbn/serverless-types/*": ["packages/serverless/types/*"], "@kbn/session-notifications-plugin": ["test/plugin_functional/plugins/session_notifications"], "@kbn/session-notifications-plugin/*": ["test/plugin_functional/plugins/session_notifications/*"], "@kbn/session-view-plugin": ["x-pack/plugins/session_view"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8071085e9cc8b..8e85011f366d8 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -62,6 +62,7 @@ "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", + "xpack.serverless": "plugins/serverless", "xpack.securitySolution": "plugins/security_solution", "xpack.sessionView": "plugins/session_view", "xpack.snapshotRestore": "plugins/snapshot_restore", diff --git a/x-pack/plugins/security/public/config.ts b/x-pack/plugins/security/public/config.ts index 440bd8da27d90..6a5a8dac01500 100644 --- a/x-pack/plugins/security/public/config.ts +++ b/x-pack/plugins/security/public/config.ts @@ -9,4 +9,5 @@ export interface ConfigType { loginAssistanceMessage: string; showInsecureClusterWarning: boolean; sameSiteCookies: 'Strict' | 'Lax' | 'None' | undefined; + showNavLinks: boolean; } diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index 91d0c33ade107..e1af50e986450 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -29,6 +29,7 @@ interface SetupDeps { securityLicense: SecurityLicense; logoutUrl: string; securityApiClients: SecurityApiClients; + showNavLinks?: boolean; } interface StartDeps { @@ -54,16 +55,18 @@ export class SecurityNavControlService { private securityApiClients!: SecurityApiClients; private navControlRegistered!: boolean; + private showNavLinks!: boolean; private securityFeaturesSubscription?: Subscription; private readonly stop$ = new ReplaySubject(1); private userMenuLinks$ = new BehaviorSubject([]); - public setup({ securityLicense, logoutUrl, securityApiClients }: SetupDeps) { + public setup({ securityLicense, logoutUrl, securityApiClients, showNavLinks = true }: SetupDeps) { this.securityLicense = securityLicense; this.logoutUrl = logoutUrl; this.securityApiClients = securityApiClients; + this.showNavLinks = showNavLinks; } public start({ core, authc }: StartDeps): SecurityNavControlServiceStart { @@ -72,7 +75,7 @@ export class SecurityNavControlService { const isAnonymousPath = core.http.anonymousPaths.isAnonymous(window.location.pathname); const shouldRegisterNavControl = - !isAnonymousPath && showLinks && !this.navControlRegistered; + this.showNavLinks && !isAnonymousPath && showLinks && !this.navControlRegistered; if (shouldRegisterNavControl) { this.registerSecurityNavControl(core, authc); } diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index c56c40f63b4d0..084a34e635dcf 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -107,6 +107,7 @@ export class SecurityPlugin securityLicense: license, logoutUrl: getLogoutUrl(core.http), securityApiClients: this.securityApiClients, + showNavLinks: this.config.showNavLinks, }); this.analyticsService.setup({ diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 8b7324e70d646..ea255d61ee255 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -60,6 +60,7 @@ describe('config schema', () => { "selector": Object {}, }, "cookieName": "sid", + "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", "public": Object {}, @@ -70,6 +71,7 @@ describe('config schema', () => { "lifespan": "P30D", }, "showInsecureClusterWarning": true, + "showNavLinks": true, } `); @@ -113,6 +115,7 @@ describe('config schema', () => { "selector": Object {}, }, "cookieName": "sid", + "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "loginAssistanceMessage": "", "public": Object {}, @@ -123,6 +126,7 @@ describe('config schema', () => { "lifespan": "P30D", }, "showInsecureClusterWarning": true, + "showNavLinks": true, } `); @@ -166,6 +170,7 @@ describe('config schema', () => { "selector": Object {}, }, "cookieName": "sid", + "enabled": true, "loginAssistanceMessage": "", "public": Object {}, "secureCookies": false, @@ -175,6 +180,7 @@ describe('config schema', () => { "lifespan": "P30D", }, "showInsecureClusterWarning": true, + "showNavLinks": true, } `); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index e3584427964f3..91abf77a376f8 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -204,6 +204,7 @@ export const ConfigSchema = schema.object({ loginAssistanceMessage: schema.string({ defaultValue: '' }), showInsecureClusterWarning: schema.boolean({ defaultValue: true }), loginHelp: schema.maybe(schema.string()), + showNavLinks: schema.boolean({ defaultValue: true }), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), @@ -295,6 +296,7 @@ export const ConfigSchema = schema.object({ ) ), }), + enabled: schema.boolean({ defaultValue: true }), }); export function createConfig( diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 89ddb41375a91..06ba1e77118e9 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -52,6 +52,7 @@ export const config: PluginConfigDescriptor> = { loginAssistanceMessage: true, showInsecureClusterWarning: true, sameSiteCookies: true, + showNavLinks: true, }, }; export const plugin: PluginInitializer< diff --git a/x-pack/plugins/serverless/README.mdx b/x-pack/plugins/serverless/README.mdx new file mode 100755 index 0000000000000..f3b9940e4b371 --- /dev/null +++ b/x-pack/plugins/serverless/README.mdx @@ -0,0 +1,22 @@ +--- +id: serverless/plugin +slug: /serverless/plugin +title: Serverless Plugin +description: The plugin responsible for managing Serverless settings and providing services to all product serverless plugins. +tags: ['serverless', 'plugin'] +date: 2023-04-23 +--- + +![diagram](./assets/diagram.png) + +a. `serverless.yml` config enables Serverless plugin, provides settings for *all* projects, (e.g. disabling Reporting). + +b. Product-specific `yml` file enables corresponding Project plugin, provides settings for a specific project, (e.g. disabling Observability). + +c. Project plugin interacts with Serverless plugin to customize Serverless Kibana. + +d. Serverless plugin interacts with Kibana Core to customize Classic Kibana. + +e. Project plugin interacts with corresponding Solution plugin to customize the Solution experience for Serverless. + +Communication occurs in a *single direction*. While it would be tempting to add a global flag to check if Serverless is enabled, doing so short-circuits the "affecting" model. \ No newline at end of file diff --git a/x-pack/plugins/serverless/assets/diagram.png b/x-pack/plugins/serverless/assets/diagram.png new file mode 100644 index 0000000000000..57dda7513bb4b Binary files /dev/null and b/x-pack/plugins/serverless/assets/diagram.png differ diff --git a/x-pack/plugins/serverless/common/index.ts b/x-pack/plugins/serverless/common/index.ts new file mode 100644 index 0000000000000..dacf50415f2a7 --- /dev/null +++ b/x-pack/plugins/serverless/common/index.ts @@ -0,0 +1,12 @@ +/* + * 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 const PLUGIN_ID = 'serverless'; +export const PLUGIN_NAME = 'serverless'; + +/** Internal API route responsible for switching between project configurations. */ +export const API_SWITCH_PROJECT = '/internal/serverless/switch_project'; diff --git a/x-pack/plugins/serverless/kibana.jsonc b/x-pack/plugins/serverless/kibana.jsonc new file mode 100644 index 0000000000000..af79877f1079f --- /dev/null +++ b/x-pack/plugins/serverless/kibana.jsonc @@ -0,0 +1,21 @@ +{ + "type": "plugin", + "id": "@kbn/serverless", + "owner": "@elastic/appex-sharedux", + "description": "The core Serverless plugin, providing APIs to Serverless Project plugins.", + "plugin": { + "id": "serverless", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "serverless", + "plugin", + ], + "requiredPlugins": [ + "kibanaReact", + ], + "optionalPlugins": [], + "requiredBundles": [] + } +} \ No newline at end of file diff --git a/x-pack/plugins/serverless/package.json b/x-pack/plugins/serverless/package.json new file mode 100644 index 0000000000000..ec457f89fbcaa --- /dev/null +++ b/x-pack/plugins/serverless/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/serverless", + "version": "1.0.0", + "license": "Elastic License 2.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", + "kbn": "node ../../scripts/kbn" + } +} \ No newline at end of file diff --git a/x-pack/plugins/serverless/public/config.ts b/x-pack/plugins/serverless/public/config.ts new file mode 100644 index 0000000000000..85c453ab50113 --- /dev/null +++ b/x-pack/plugins/serverless/public/config.ts @@ -0,0 +1,15 @@ +/* + * 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 interface ServerlessConfig { + developer?: { + projectSwitcher?: { + enabled: boolean; + currentType: 'security' | 'observability' | 'search'; + }; + }; +} diff --git a/x-pack/plugins/serverless/public/index.ts b/x-pack/plugins/serverless/public/index.ts new file mode 100644 index 0000000000000..7f841ef25be2f --- /dev/null +++ b/x-pack/plugins/serverless/public/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/public'; +import { ServerlessPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ServerlessPlugin(initializerContext); +} + +export type { ServerlessPluginSetup, ServerlessPluginStart } from './types'; diff --git a/x-pack/plugins/serverless/public/plugin.tsx b/x-pack/plugins/serverless/public/plugin.tsx new file mode 100644 index 0000000000000..9c9debe3e9f21 --- /dev/null +++ b/x-pack/plugins/serverless/public/plugin.tsx @@ -0,0 +1,65 @@ +/* + * 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 ReactDOM from 'react-dom'; + +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import { ProjectSwitcher, ProjectSwitcherKibanaProvider } from '@kbn/serverless-project-switcher'; +import { ProjectType } from '@kbn/serverless-types'; + +import { ServerlessPluginSetup, ServerlessPluginStart } from './types'; +import { ServerlessConfig } from './config'; +import { API_SWITCH_PROJECT as projectChangeAPIUrl } from '../common'; + +export class ServerlessPlugin implements Plugin { + private readonly config: ServerlessConfig; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get(); + } + + public setup(_core: CoreSetup): ServerlessPluginSetup { + return {}; + } + + public start(core: CoreStart): ServerlessPluginStart { + const { developer } = this.config; + + if (developer && developer.projectSwitcher && developer.projectSwitcher.enabled) { + const { currentType } = developer.projectSwitcher; + + core.chrome.navControls.registerRight({ + mount: (target) => this.mountProjectSwitcher(target, core, currentType), + }); + } + + core.chrome.setChromeStyle('project'); + + return {}; + } + + public stop() {} + + private mountProjectSwitcher( + targetDomElement: HTMLElement, + coreStart: CoreStart, + currentProjectType: ProjectType + ) { + ReactDOM.render( + + + + + , + targetDomElement + ); + + return () => ReactDOM.unmountComponentAtNode(targetDomElement); + } +} diff --git a/x-pack/plugins/serverless/public/types.ts b/x-pack/plugins/serverless/public/types.ts new file mode 100644 index 0000000000000..92a804b34a948 --- /dev/null +++ b/x-pack/plugins/serverless/public/types.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerlessPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerlessPluginStart {} diff --git a/x-pack/plugins/serverless/server/config.ts b/x-pack/plugins/serverless/server/config.ts new file mode 100644 index 0000000000000..96c4816bd40f0 --- /dev/null +++ b/x-pack/plugins/serverless/server/config.ts @@ -0,0 +1,58 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +export * from './types'; + +const configSchema = schema.object({ + // Is this plugin enabled? + enabled: schema.boolean({ defaultValue: false }), + + // Config namespace for developer-specific settings. + developer: schema.maybe( + schema.object({ + // Settings for the project switcher. + projectSwitcher: schema.maybe( + schema.object({ + // Should the switcher be enabled? + enabled: schema.conditional( + schema.contextRef('dev'), + false, + schema.boolean({ + validate: (rawValue) => { + if (rawValue === true) { + return 'Switcher can only be enabled in development mode'; + } + }, + defaultValue: false, + }), + schema.boolean({ defaultValue: true }) + ), + // Which project is currently selected? + currentType: schema.oneOf([ + schema.literal('security'), + schema.literal('observability'), + schema.literal('search'), + ]), + }) + ), + }) + ), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + developer: true, + }, +}; + +export type ServerlessConfig = TypeOf; diff --git a/x-pack/plugins/serverless/server/index.ts b/x-pack/plugins/serverless/server/index.ts new file mode 100644 index 0000000000000..04f08ac6ed4cc --- /dev/null +++ b/x-pack/plugins/serverless/server/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginInitializerContext } from '@kbn/core/server'; +import { ServerlessPlugin } from './plugin'; +export { config } from './config'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new ServerlessPlugin(initializerContext); +}; + +export type { ServerlessPluginSetup, ServerlessPluginStart } from './types'; diff --git a/x-pack/plugins/serverless/server/plugin.ts b/x-pack/plugins/serverless/server/plugin.ts new file mode 100644 index 0000000000000..aadeb86594ec7 --- /dev/null +++ b/x-pack/plugins/serverless/server/plugin.ts @@ -0,0 +1,92 @@ +/* + * 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 { existsSync, readFileSync, writeFileSync } from 'fs'; +import { resolve } from 'path'; + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { getConfigDirectory } from '@kbn/utils'; +import { ProjectType } from '@kbn/serverless-types'; + +import { ServerlessPluginSetup, ServerlessPluginStart } from './types'; +import { ServerlessConfig } from './config'; +import { API_SWITCH_PROJECT } from '../common'; + +const switchBodySchema = schema.object({ + id: schema.oneOf([ + schema.literal('observability'), + schema.literal('security'), + schema.literal('search'), + ]), +}); + +type SwitchReqBody = TypeOf; + +const typeToIdMap: Record = { + observability: 'oblt', + security: 'security', + search: 'es', +}; + +export class ServerlessPlugin implements Plugin { + private readonly config: ServerlessConfig; + + constructor(private readonly context: PluginInitializerContext) { + this.config = this.context.config.get(); + } + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + const { developer } = this.config; + + // If we're in development mode, and the switcher is enabled, register the + // API endpoint responsible for switching projects. + if (process.env.NODE_ENV !== 'production' && developer?.projectSwitcher?.enabled) { + router.post( + { + path: API_SWITCH_PROJECT, + validate: { + body: switchBodySchema, + }, + }, + async (_context, request, response) => { + const { id } = request.body; + const path = resolve(getConfigDirectory(), `serverless.${typeToIdMap[id]}.yml`); + + try { + if (existsSync(path)) { + const data = readFileSync(path, 'utf8'); + + // The switcher is not enabled by default, in cases where one has started Serverless + // with a specific config. So in this case, to ensure the switcher remains enabled, + // erite the selected config to `recent` and tack on the setting to enable the switcher. + writeFileSync( + resolve(getConfigDirectory(), 'serverless.recent.yml'), + `${data}\nxpack.serverless.plugin.developer.projectSwitcher.enabled: true\n` + ); + + return response.ok({ body: id }); + } + } catch (e) { + return response.badRequest({ body: e }); + } + + return response.badRequest(); + } + ); + } + + return {}; + } + + public start(_core: CoreStart) { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/serverless/server/types.ts b/x-pack/plugins/serverless/server/types.ts new file mode 100644 index 0000000000000..92a804b34a948 --- /dev/null +++ b/x-pack/plugins/serverless/server/types.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerlessPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ServerlessPluginStart {} diff --git a/x-pack/plugins/serverless/tsconfig.json b/x-pack/plugins/serverless/tsconfig.json new file mode 100644 index 0000000000000..91ea373bb4270 --- /dev/null +++ b/x-pack/plugins/serverless/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/config-schema", + "@kbn/kibana-react-plugin", + "@kbn/utils", + "@kbn/serverless-project-switcher", + "@kbn/serverless-types", + ] +} diff --git a/yarn.lock b/yarn.lock index 83f9b163487d8..9cef92ef04d8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5013,6 +5013,22 @@ version "0.0.0" uid "" +"@kbn/serverless-project-switcher@link:packages/serverless/project_switcher": + version "0.0.0" + uid "" + +"@kbn/serverless-storybook-config@link:packages/serverless/storybook/config": + version "0.0.0" + uid "" + +"@kbn/serverless-types@link:packages/serverless/types": + version "0.0.0" + uid "" + +"@kbn/serverless@link:x-pack/plugins/serverless": + version "0.0.0" + uid "" + "@kbn/session-notifications-plugin@link:test/plugin_functional/plugins/session_notifications": version "0.0.0" uid ""