mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[serverless] Create the Serverless Plugin (#155582)
> Derived from https://github.com/elastic/kibana/pull/153274 for production. ## Summary This PR creates the `serverless` plugin for Kibana Serverless projects.  It uses the methodology proven out in the proof-of-concept (https://github.com/elastic/kibana/pull/153274) and prepares it for production: - Adds chrome style and related API to the `chrome` services. - Creates the `serverless` plugin. - Invokes the new chrome style API for all serverless projects. - Alters `yarn` scripts to support all project types, and switching between them. - Creates the new "Project Switcher" component for use in the new chrome header for Serverless. - Creates a Storybook config for this and future components. - Adds API endpoint to trigger project switching and `Watcher` restarts. <img width="1598" alt="Screenshot 2023-04-26 at 10 44 01 AM" src="https://user-images.githubusercontent.com/297604/234612654-fdcf38ea-8c48-4066-bc85-507f40c984aa.png"> ## Next steps - [x] Creating a PR for enabling/disabling related plugins for Serverless. (https://github.com/elastic/kibana/pull/155583) - [ ] Creating product plugin PR based on https://github.com/elastic/kibana/pull/153274. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5e713fb225
commit
8e37b38417
83 changed files with 1623 additions and 58 deletions
|
@ -43,6 +43,7 @@ const STORYBOOKS = [
|
|||
'observability',
|
||||
'presentation',
|
||||
'security_solution',
|
||||
'serverless',
|
||||
'shared_ux',
|
||||
'triggers_actions_ui',
|
||||
'ui_actions_enhanced',
|
||||
|
|
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
xpack.serverless.plugin.developer.projectSwitcher.currentType: 'search'
|
|
@ -1 +1,2 @@
|
|||
xpack.infra.logs.app_target: discover
|
||||
xpack.serverless.plugin.developer.projectSwitcher.currentType: 'observability'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
xpack.serverless.plugin.developer.projectSwitcher.currentType: 'security'
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ChromeNavLink | undefined>(undefined);
|
||||
const helpSupportUrl$ = new BehaviorSubject<string>(KIBANA_ASK_ELASTIC_LINK);
|
||||
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
|
||||
const chromeStyle$ = new BehaviorSubject<ChromeStyle>('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<ChromeUserBanner | undefined>(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 (
|
||||
<ProjectHeader
|
||||
{...{
|
||||
application,
|
||||
globalHelpExtensionMenuLinks$,
|
||||
}}
|
||||
actionMenu$={application.currentActionMenu$}
|
||||
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
|
||||
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
|
||||
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
|
||||
navControlsRight$={navControls.getRight$()}
|
||||
kibanaDocLink={docLinks.links.kibana.guide}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Header
|
||||
loadingCount$={http.getLoadingCount$()}
|
||||
application={application}
|
||||
headerBanner$={headerBanner$.pipe(takeUntil(this.stop$))}
|
||||
badge$={badge$.pipe(takeUntil(this.stop$))}
|
||||
basePath={http.basePath}
|
||||
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
|
||||
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))}
|
||||
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
|
||||
kibanaDocLink={docLinks.links.kibana.guide}
|
||||
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
|
||||
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
|
||||
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
|
||||
homeHref={http.basePath.prepend('/app/home')}
|
||||
isVisible$={this.isVisible$}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
navLinks$={navLinks.getNavLinks$()}
|
||||
recentlyAccessed$={recentlyAccessed.get$()}
|
||||
navControlsLeft$={navControls.getLeft$()}
|
||||
navControlsCenter$={navControls.getCenter$()}
|
||||
navControlsRight$={navControls.getRight$()}
|
||||
navControlsExtension$={navControls.getExtension$()}
|
||||
onIsLockedUpdate={setIsNavDrawerLocked}
|
||||
isLocked$={getIsNavDrawerLocked$}
|
||||
customBranding$={customBranding$}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return <Component {...{ style$: chromeStyle$ }} />;
|
||||
};
|
||||
|
||||
return {
|
||||
navControls,
|
||||
navLinks,
|
||||
recentlyAccessed,
|
||||
docTitle,
|
||||
|
||||
getHeaderComponent: () => (
|
||||
<Header
|
||||
loadingCount$={http.getLoadingCount$()}
|
||||
application={application}
|
||||
headerBanner$={headerBanner$.pipe(takeUntil(this.stop$))}
|
||||
badge$={badge$.pipe(takeUntil(this.stop$))}
|
||||
basePath={http.basePath}
|
||||
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
|
||||
breadcrumbsAppendExtension$={breadcrumbsAppendExtension$.pipe(takeUntil(this.stop$))}
|
||||
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
|
||||
kibanaDocLink={docLinks.links.kibana.guide}
|
||||
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
|
||||
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
|
||||
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
|
||||
homeHref={http.basePath.prepend('/app/home')}
|
||||
isVisible$={this.isVisible$}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
navLinks$={navLinks.getNavLinks$()}
|
||||
recentlyAccessed$={recentlyAccessed.get$()}
|
||||
navControlsLeft$={navControls.getLeft$()}
|
||||
navControlsCenter$={navControls.getCenter$()}
|
||||
navControlsRight$={navControls.getRight$()}
|
||||
navControlsExtension$={navControls.getExtension$()}
|
||||
onIsLockedUpdate={setIsNavDrawerLocked}
|
||||
isLocked$={getIsNavDrawerLocked$}
|
||||
customBranding$={customBranding$}
|
||||
/>
|
||||
),
|
||||
getHeaderComponent,
|
||||
|
||||
getIsVisible$: () => this.isVisible$,
|
||||
|
||||
|
@ -302,6 +333,8 @@ export class ChromeService {
|
|||
},
|
||||
|
||||
getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)),
|
||||
setChromeStyle,
|
||||
getChromeStyle$: () => chromeStyle$.pipe(takeUntil(this.stop$)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
*/
|
||||
|
||||
export { Header } from './header';
|
||||
export { ProjectHeader } from './project';
|
||||
export { LoadingIndicator } from './loading_indicator';
|
||||
export type { NavType } from './header';
|
||||
|
|
|
@ -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<ChromeBreadcrumb[]>;
|
||||
actionMenu$: Observable<MountPoint | undefined>;
|
||||
kibanaDocLink: string;
|
||||
globalHelpExtensionMenuLinks$: Observable<ChromeGlobalHelpExtensionMenuLink[]>;
|
||||
helpExtension$: Observable<ChromeHelpExtension | undefined>;
|
||||
helpSupportUrl$: Observable<string>;
|
||||
kibanaVersion: string;
|
||||
application: InternalApplicationStart;
|
||||
navControlsRight$: Observable<ChromeNavControl[]>;
|
||||
}
|
||||
|
||||
export const ProjectHeader = ({
|
||||
application,
|
||||
kibanaDocLink,
|
||||
kibanaVersion,
|
||||
...observables
|
||||
}: Props) => {
|
||||
const renderLogo = () => (
|
||||
<EuiHeaderLogo
|
||||
iconType="logoElastic"
|
||||
href="#"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
aria-label="Go to home page"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiHeader position="fixed">
|
||||
<EuiHeaderSection grow={false}>
|
||||
<EuiHeaderSectionItem border="right">{renderLogo()}</EuiHeaderSectionItem>
|
||||
<EuiHeaderSectionItem>
|
||||
<HeaderBreadcrumbs breadcrumbs$={observables.breadcrumbs$} />
|
||||
</EuiHeaderSectionItem>
|
||||
</EuiHeaderSection>
|
||||
<EuiHeaderSection side="right">
|
||||
<EuiHeaderSectionItem>
|
||||
<HeaderHelpMenu
|
||||
globalHelpExtensionMenuLinks$={observables.globalHelpExtensionMenuLinks$}
|
||||
helpExtension$={observables.helpExtension$}
|
||||
helpSupportUrl$={observables.helpSupportUrl$}
|
||||
kibanaDocLink={kibanaDocLink}
|
||||
kibanaVersion={kibanaVersion}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
/>
|
||||
</EuiHeaderSectionItem>
|
||||
<EuiHeaderSectionItem border="left">
|
||||
<HeaderActionMenu actionMenu$={observables.actionMenu$} />
|
||||
</EuiHeaderSectionItem>
|
||||
|
||||
<EuiHeaderSectionItem>
|
||||
<HeaderNavControls navControls$={observables.navControlsRight$} />
|
||||
</EuiHeaderSectionItem>
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
<Router history={application.history}>
|
||||
<ProjectNavigation>
|
||||
<span />
|
||||
</ProjectNavigation>
|
||||
</Router>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 (
|
||||
<EuiThemeProvider colorMode={colorMode === 'DARK' ? 'LIGHT' : 'DARK'}>
|
||||
<EuiCollapsibleNav
|
||||
css={collabsibleNavCSS}
|
||||
isOpen={true}
|
||||
showButtonIfDocked={true}
|
||||
onClose={toggleOpen}
|
||||
isDocked={true}
|
||||
size={isOpen ? SIZE_OPEN : SIZE_CLOSED}
|
||||
hideCloseButton={false}
|
||||
button={
|
||||
<span css={buttonCSS}>
|
||||
<EuiButtonIcon
|
||||
iconType={isOpen ? 'menuLeft' : 'menuRight'}
|
||||
aria-label={isOpen ? openAriaLabel : closedAriaLabel}
|
||||
color="text"
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{isOpen && children}
|
||||
</EuiCollapsibleNav>
|
||||
</EuiThemeProvider>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,10 @@
|
|||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
"react",
|
||||
"@kbn/ambient-ui-types",
|
||||
"@kbn/ambient-storybook-types",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -7,25 +7,26 @@
|
|||
*/
|
||||
|
||||
export type {
|
||||
ChromeUserBanner,
|
||||
ChromeBadge,
|
||||
ChromeBreadcrumb,
|
||||
ChromeHelpExtension,
|
||||
ChromeHelpExtensionMenuLink,
|
||||
ChromeHelpExtensionLinkBase,
|
||||
ChromeHelpMenuActions,
|
||||
ChromeNavLink,
|
||||
ChromeBreadcrumbsAppendExtension,
|
||||
ChromeNavLinks,
|
||||
ChromeDocTitle,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
ChromeHelpExtension,
|
||||
ChromeHelpExtensionLinkBase,
|
||||
ChromeHelpExtensionMenuCustomLink,
|
||||
ChromeHelpExtensionMenuDiscussLink,
|
||||
ChromeHelpExtensionMenuDocumentationLink,
|
||||
ChromeHelpExtensionMenuGitHubLink,
|
||||
ChromeHelpExtensionMenuLink,
|
||||
ChromeHelpMenuActions,
|
||||
ChromeNavControl,
|
||||
ChromeNavControls,
|
||||
ChromeBadge,
|
||||
ChromeHelpExtensionMenuGitHubLink,
|
||||
ChromeHelpExtensionMenuDocumentationLink,
|
||||
ChromeHelpExtensionMenuDiscussLink,
|
||||
ChromeHelpExtensionMenuCustomLink,
|
||||
ChromeGlobalHelpExtensionMenuLink,
|
||||
ChromeDocTitle,
|
||||
ChromeStart,
|
||||
ChromeNavLink,
|
||||
ChromeNavLinks,
|
||||
ChromeRecentlyAccessed,
|
||||
ChromeRecentlyAccessedHistoryItem,
|
||||
ChromeStart,
|
||||
ChromeStyle,
|
||||
ChromeUserBanner,
|
||||
} from './src';
|
||||
|
|
|
@ -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<boolean>;
|
||||
|
||||
/**
|
||||
* 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<ChromeStyle>;
|
||||
}
|
||||
|
|
|
@ -26,4 +26,4 @@ export type {
|
|||
ChromeRecentlyAccessed,
|
||||
ChromeRecentlyAccessedHistoryItem,
|
||||
} from './recently_accessed';
|
||||
export type { ChromeBadge, ChromeUserBanner } from './types';
|
||||
export type { ChromeBadge, ChromeUserBanner, ChromeStyle } from './types';
|
||||
|
|
|
@ -20,3 +20,6 @@ export interface ChromeBadge {
|
|||
export interface ChromeUserBanner {
|
||||
content: MountPoint<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ChromeStyle = 'classic' | 'project';
|
||||
|
|
|
@ -115,6 +115,7 @@ pageLoadAssetSize:
|
|||
searchprofiler: 67080
|
||||
security: 65433
|
||||
securitySolution: 66738
|
||||
serverless: 16573
|
||||
sessionView: 77750
|
||||
share: 71239
|
||||
snapshotRestore: 79032
|
||||
|
|
12
packages/serverless/project_switcher/README.mdx
Normal file
12
packages/serverless/project_switcher/README.mdx
Normal file
|
@ -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.
|
11
packages/serverless/project_switcher/index.ts
Normal file
11
packages/serverless/project_switcher/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { ProjectSwitcherProps, KibanaDependencies } from './src';
|
||||
|
||||
export { ProjectSwitcher, ProjectSwitcherKibanaProvider, ProjectSwitcherProvider } from './src';
|
13
packages/serverless/project_switcher/jest.config.js
Normal file
13
packages/serverless/project_switcher/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/serverless/project_switcher'],
|
||||
};
|
5
packages/serverless/project_switcher/kibana.jsonc
Normal file
5
packages/serverless/project_switcher/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/serverless-project-switcher",
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
23
packages/serverless/project_switcher/mocks/jest.mock.ts
Normal file
23
packages/serverless/project_switcher/mocks/jest.mock.ts
Normal file
|
@ -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<Services> = () => ({
|
||||
setProjectType: jest.fn(),
|
||||
});
|
||||
|
||||
export const getProjectSwitcherKibanaDependenciesMock: () => jest.Mocked<KibanaDependencies> =
|
||||
() => ({
|
||||
coreStart: {
|
||||
http: {
|
||||
post: jest.fn(() => Promise.resolve({ data: {} })),
|
||||
},
|
||||
},
|
||||
projectChangeAPIUrl: 'serverless/change_project',
|
||||
});
|
51
packages/serverless/project_switcher/mocks/storybook.mock.ts
Normal file
51
packages/serverless/project_switcher/mocks/storybook.mock.ts
Normal file
|
@ -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<ProjectSwitcherProps, 'currentProjectType'>;
|
||||
|
||||
/**
|
||||
* Storybook parameters provided from the controls addon.
|
||||
*/
|
||||
export type ProjectSwitcherStorybookParams = Record<keyof PropArguments, any>;
|
||||
|
||||
/**
|
||||
* 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'),
|
||||
};
|
||||
}
|
||||
}
|
6
packages/serverless/project_switcher/package.json
Normal file
6
packages/serverless/project_switcher/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/serverless-project-switcher",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
24
packages/serverless/project_switcher/src/constants.ts
Normal file
24
packages/serverless/project_switcher/src/constants.ts
Normal file
|
@ -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<ProjectType, IconType> = {
|
||||
observability: 'logoObservability',
|
||||
security: 'logoSecurity',
|
||||
search: 'logoEnterpriseSearch',
|
||||
} as const;
|
||||
|
||||
export const labels: Record<ProjectType, string> = {
|
||||
observability: 'Observability',
|
||||
security: 'Security',
|
||||
search: 'Enterprise Search',
|
||||
} as const;
|
||||
|
||||
export const projectTypes: ProjectType[] = ['security', 'observability', 'search'];
|
31
packages/serverless/project_switcher/src/header_button.tsx
Normal file
31
packages/serverless/project_switcher/src/header_button.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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<HTMLButtonElement>;
|
||||
currentProjectType: ProjectType;
|
||||
}
|
||||
|
||||
export const HeaderButton = ({ onClick, currentProjectType }: Props) => (
|
||||
<EuiHeaderSectionItemButton
|
||||
aria-label="Developer Tools"
|
||||
data-test-subj={TEST_ID}
|
||||
{...{ onClick }}
|
||||
>
|
||||
<EuiIcon type={icons[currentProjectType]} size="m" />
|
||||
</EuiHeaderSectionItemButton>
|
||||
);
|
12
packages/serverless/project_switcher/src/index.ts
Normal file
12
packages/serverless/project_switcher/src/index.ts
Normal file
|
@ -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';
|
33
packages/serverless/project_switcher/src/item.tsx
Normal file
33
packages/serverless/project_switcher/src/item.tsx
Normal file
|
@ -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<EuiIconProps, 'type'> {
|
||||
type: ProjectType;
|
||||
onChange: (type: ProjectType) => void;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export const SwitcherItem = ({ type: id, onChange, isSelected }: ItemProps) => (
|
||||
<EuiKeyPadMenuItem
|
||||
checkable="single"
|
||||
name="projectSelection"
|
||||
label={labels[id]}
|
||||
onChange={onChange as OnChangeType}
|
||||
{...{ isSelected, id }}
|
||||
>
|
||||
<EuiIcon type={icons[id]} size="l" />
|
||||
</EuiKeyPadMenuItem>
|
||||
);
|
21
packages/serverless/project_switcher/src/loader.tsx
Normal file
21
packages/serverless/project_switcher/src/loader.tsx
Normal file
|
@ -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) => (
|
||||
<div className="kbnWelcomeView" id="kbn_loading_message" data-test-subj="kbnLoadingMessage">
|
||||
<div className="kbnLoaderWrap">
|
||||
<Logo {...props} />
|
||||
<div className="kbnWelcomeText">Loading Project</div>
|
||||
<div className="kbnProgress" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
32
packages/serverless/project_switcher/src/logo.tsx
Normal file
32
packages/serverless/project_switcher/src/logo.tsx
Normal file
|
@ -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 <EuiIcon type={type} size="xxl" />;
|
||||
};
|
63
packages/serverless/project_switcher/src/services.tsx
Normal file
63
packages/serverless/project_switcher/src/services.tsx
Normal file
|
@ -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<Services | null>(null);
|
||||
|
||||
/**
|
||||
* A Context Provider that provides services to the component and its dependencies.
|
||||
*/
|
||||
export const ProjectSwitcherProvider: FC<Services> = ({ children, ...services }) => {
|
||||
return <Context.Provider value={services}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kibana-specific Provider that maps dependencies to services.
|
||||
*/
|
||||
export const ProjectSwitcherKibanaProvider: FC<KibanaDependencies> = ({
|
||||
children,
|
||||
coreStart,
|
||||
projectChangeAPIUrl,
|
||||
}) => {
|
||||
const value: Services = {
|
||||
setProjectType: (projectType) => {
|
||||
coreStart.http
|
||||
.post(projectChangeAPIUrl, { body: JSON.stringify({ id: projectType }) })
|
||||
.then(() => {
|
||||
ReactDOM.render(<Loader project={projectType} />, document.body);
|
||||
|
||||
// Give the watcher a couple of seconds to see the file change.
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return <Context.Provider {...{ value }}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
|
@ -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) => (
|
||||
<SwitcherItem
|
||||
key={type}
|
||||
type={type}
|
||||
onChange={onChange}
|
||||
isSelected={currentProjectType === type}
|
||||
/>
|
||||
));
|
||||
|
||||
const button = <HeaderButton onClick={onButtonClick} {...{ currentProjectType }} />;
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
{...{ id, button, isOpen, closePopover }}
|
||||
anchorPosition="downRight"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiPopoverTitle>Switch Project Type</EuiPopoverTitle>
|
||||
<EuiKeyPadMenu css={switcherCSS} data-test-subj={TEST_ID_ITEM_GROUP}>
|
||||
{items}
|
||||
</EuiKeyPadMenu>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<Provider {...mock.getServices(params)}>
|
||||
<Component {...params} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectSwitcher.argTypes = argTypes;
|
151
packages/serverless/project_switcher/src/switcher.test.tsx
Normal file
151
packages/serverless/project_switcher/src/switcher.test.tsx
Normal file
|
@ -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<KibanaDependencies>] => {
|
||||
const mock = getProjectSwitcherKibanaDependenciesMock();
|
||||
return [
|
||||
render(
|
||||
<ProjectSwitcherKibanaProvider {...mock}>
|
||||
<ProjectSwitcher {...{ currentProjectType }} />
|
||||
</ProjectSwitcherKibanaProvider>
|
||||
),
|
||||
mock,
|
||||
];
|
||||
};
|
||||
|
||||
const renderProjectSwitcher = (
|
||||
currentProjectType: ProjectType = 'observability'
|
||||
): [RenderResult, jest.Mocked<Services>] => {
|
||||
const mock = getProjectSwitcherServicesMock();
|
||||
return [
|
||||
render(
|
||||
<ProjectSwitcherProvider {...mock}>
|
||||
<ProjectSwitcher {...{ currentProjectType }} />
|
||||
</ProjectSwitcherProvider>
|
||||
),
|
||||
mock,
|
||||
];
|
||||
};
|
||||
|
||||
describe('ProjectSwitcher', () => {
|
||||
describe('Component', () => {
|
||||
test('is rendered', () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<ProjectSwitcherComponent
|
||||
currentProjectType="observability"
|
||||
onProjectChange={jest.fn()}
|
||||
/>
|
||||
)
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Connected Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test("doesn't render if the Provider is missing", () => {
|
||||
expect(() => render(<ProjectSwitcher currentProjectType="observability" />)).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();
|
||||
});
|
||||
});
|
||||
});
|
21
packages/serverless/project_switcher/src/switcher.tsx
Normal file
21
packages/serverless/project_switcher/src/switcher.tsx
Normal file
|
@ -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 <Component {...{ onProjectChange, ...props }} />;
|
||||
};
|
39
packages/serverless/project_switcher/src/types.ts
Normal file
39
packages/serverless/project_switcher/src/types.ts
Normal file
|
@ -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<unknown>;
|
||||
};
|
||||
};
|
||||
projectChangeAPIUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the `ProjectSwitcher` pure component.
|
||||
*/
|
||||
export interface ProjectSwitcherComponentProps {
|
||||
onProjectChange: (projectType: ProjectType) => void;
|
||||
currentProjectType: ProjectType;
|
||||
}
|
||||
|
||||
export type ProjectSwitcherProps = Pick<ProjectSwitcherComponentProps, 'currentProjectType'>;
|
23
packages/serverless/project_switcher/tsconfig.json
Normal file
23
packages/serverless/project_switcher/tsconfig.json
Normal file
|
@ -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",
|
||||
]
|
||||
}
|
5
packages/serverless/storybook/config/README.mdx
Normal file
5
packages/serverless/storybook/config/README.mdx
Normal file
|
@ -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.
|
13
packages/serverless/storybook/config/constants.ts
Normal file
13
packages/serverless/storybook/config/constants.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/** 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';
|
9
packages/serverless/storybook/config/index.ts
Executable file
9
packages/serverless/storybook/config/index.ts
Executable file
|
@ -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';
|
6
packages/serverless/storybook/config/kibana.jsonc
Normal file
6
packages/serverless/storybook/config/kibana.jsonc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/serverless-storybook-config",
|
||||
"owner": "@elastic/appex-sharedux",
|
||||
"devOnly": true
|
||||
}
|
17
packages/serverless/storybook/config/main.ts
Normal file
17
packages/serverless/storybook/config/main.ts
Normal file
|
@ -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,
|
||||
},
|
||||
};
|
23
packages/serverless/storybook/config/manager.ts
Normal file
23
packages/serverless/storybook/config/manager.ts
Normal file
|
@ -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,
|
||||
});
|
6
packages/serverless/storybook/config/package.json
Normal file
6
packages/serverless/storybook/config/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/serverless-storybook-config",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
22
packages/serverless/storybook/config/preview.ts
Normal file
22
packages/serverless/storybook/config/preview.ts
Normal file
|
@ -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;
|
19
packages/serverless/storybook/config/tsconfig.json
Normal file
19
packages/serverless/storybook/config/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/storybook",
|
||||
]
|
||||
}
|
10
packages/serverless/types/README.mdx
Normal file
10
packages/serverless/types/README.mdx
Normal file
|
@ -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.
|
9
packages/serverless/types/index.d.ts
vendored
Normal file
9
packages/serverless/types/index.d.ts
vendored
Normal file
|
@ -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';
|
5
packages/serverless/types/kibana.jsonc
Normal file
5
packages/serverless/types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/serverless-types",
|
||||
"owner": "@elastic/appex-sharedux"
|
||||
}
|
6
packages/serverless/types/package.json
Normal file
6
packages/serverless/types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/serverless-types",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
17
packages/serverless/types/tsconfig.json
Normal file
17
packages/serverless/types/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
}
|
|
@ -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 <oblt|security|es>', 'Start Kibana in a serverless project mode');
|
||||
command
|
||||
.option(
|
||||
'--serverless',
|
||||
'Start Kibana in the most recent serverless project mode, (default is es)'
|
||||
)
|
||||
.option(
|
||||
'--serverless <oblt|security|es>',
|
||||
'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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,9 @@
|
|||
&.kbnBody--chromeHidden {
|
||||
@include kbnAffordForHeader(0);
|
||||
}
|
||||
&.kbnBody--projectLayout {
|
||||
@include kbnAffordForHeader($euiHeaderHeightCompensation);
|
||||
}
|
||||
&.kbnBody--chromeHidden.kbnBody--hasHeaderBanner {
|
||||
@include kbnAffordForHeader($kbnHeaderBannerHeight);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -9,4 +9,5 @@ export interface ConfigType {
|
|||
loginAssistanceMessage: string;
|
||||
showInsecureClusterWarning: boolean;
|
||||
sameSiteCookies: 'Strict' | 'Lax' | 'None' | undefined;
|
||||
showNavLinks: boolean;
|
||||
}
|
||||
|
|
|
@ -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<void>(1);
|
||||
private userMenuLinks$ = new BehaviorSubject<UserMenuLink[]>([]);
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ export class SecurityPlugin
|
|||
securityLicense: license,
|
||||
logoutUrl: getLogoutUrl(core.http),
|
||||
securityApiClients: this.securityApiClients,
|
||||
showNavLinks: this.config.showNavLinks,
|
||||
});
|
||||
|
||||
this.analyticsService.setup({
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -52,6 +52,7 @@ export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
|
|||
loginAssistanceMessage: true,
|
||||
showInsecureClusterWarning: true,
|
||||
sameSiteCookies: true,
|
||||
showNavLinks: true,
|
||||
},
|
||||
};
|
||||
export const plugin: PluginInitializer<
|
||||
|
|
22
x-pack/plugins/serverless/README.mdx
Executable file
22
x-pack/plugins/serverless/README.mdx
Executable file
|
@ -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
|
||||
---
|
||||
|
||||

|
||||
|
||||
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.
|
BIN
x-pack/plugins/serverless/assets/diagram.png
Normal file
BIN
x-pack/plugins/serverless/assets/diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 428 KiB |
12
x-pack/plugins/serverless/common/index.ts
Normal file
12
x-pack/plugins/serverless/common/index.ts
Normal file
|
@ -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';
|
21
x-pack/plugins/serverless/kibana.jsonc
Normal file
21
x-pack/plugins/serverless/kibana.jsonc
Normal file
|
@ -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": []
|
||||
}
|
||||
}
|
11
x-pack/plugins/serverless/package.json
Normal file
11
x-pack/plugins/serverless/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
15
x-pack/plugins/serverless/public/config.ts
Normal file
15
x-pack/plugins/serverless/public/config.ts
Normal file
|
@ -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';
|
||||
};
|
||||
};
|
||||
}
|
15
x-pack/plugins/serverless/public/index.ts
Normal file
15
x-pack/plugins/serverless/public/index.ts
Normal file
|
@ -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';
|
65
x-pack/plugins/serverless/public/plugin.tsx
Normal file
65
x-pack/plugins/serverless/public/plugin.tsx
Normal file
|
@ -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<ServerlessPluginSetup, ServerlessPluginStart> {
|
||||
private readonly config: ServerlessConfig;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ServerlessConfig>();
|
||||
}
|
||||
|
||||
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(
|
||||
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
|
||||
<ProjectSwitcherKibanaProvider {...{ coreStart, projectChangeAPIUrl }}>
|
||||
<ProjectSwitcher {...{ currentProjectType }} />
|
||||
</ProjectSwitcherKibanaProvider>
|
||||
</KibanaThemeProvider>,
|
||||
targetDomElement
|
||||
);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(targetDomElement);
|
||||
}
|
||||
}
|
12
x-pack/plugins/serverless/public/types.ts
Normal file
12
x-pack/plugins/serverless/public/types.ts
Normal file
|
@ -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 {}
|
58
x-pack/plugins/serverless/server/config.ts
Normal file
58
x-pack/plugins/serverless/server/config.ts
Normal file
|
@ -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<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
schema: configSchema,
|
||||
exposeToBrowser: {
|
||||
developer: true,
|
||||
},
|
||||
};
|
||||
|
||||
export type ServerlessConfig = TypeOf<typeof configSchema>;
|
16
x-pack/plugins/serverless/server/index.ts
Normal file
16
x-pack/plugins/serverless/server/index.ts
Normal file
|
@ -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';
|
92
x-pack/plugins/serverless/server/plugin.ts
Normal file
92
x-pack/plugins/serverless/server/plugin.ts
Normal file
|
@ -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<typeof switchBodySchema>;
|
||||
|
||||
const typeToIdMap: Record<ProjectType, string> = {
|
||||
observability: 'oblt',
|
||||
security: 'security',
|
||||
search: 'es',
|
||||
};
|
||||
|
||||
export class ServerlessPlugin implements Plugin<ServerlessPluginSetup, ServerlessPluginStart> {
|
||||
private readonly config: ServerlessConfig;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
this.config = this.context.config.get<ServerlessConfig>();
|
||||
}
|
||||
|
||||
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<void, void, SwitchReqBody>(
|
||||
{
|
||||
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() {}
|
||||
}
|
12
x-pack/plugins/serverless/server/types.ts
Normal file
12
x-pack/plugins/serverless/server/types.ts
Normal file
|
@ -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 {}
|
24
x-pack/plugins/serverless/tsconfig.json
Normal file
24
x-pack/plugins/serverless/tsconfig.json
Normal file
|
@ -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",
|
||||
]
|
||||
}
|
16
yarn.lock
16
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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue