From 4d6cd9339a153d13b98668fbee6a50dc3d2a6746 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Sat, 8 Jan 2022 16:11:08 -0500 Subject: [PATCH] [shared-ux] Create Services, migrate Exit Full Screen button --- .../shared_ux/.storybook/decorators.tsx | 20 ++ src/plugins/shared_ux/.storybook/preview.ts | 12 + src/plugins/shared_ux/jest.config.js | 19 ++ .../exit_full_screen_button.test.tsx.snap | 249 ++++++++++++++++++ .../exit_full_screen_button.component.tsx | 103 ++++++++ .../exit_full_screen_button.mdx | 20 ++ .../exit_full_screen_button.stories.tsx | 40 +++ .../exit_full_screen_button.test.tsx | 77 ++++++ .../exit_full_screen_button.tsx | 71 +++++ .../exit_full_screen_button/index.ts | 14 + .../shared_ux/public/components/index.ts | 23 ++ .../public/components/utility/fallback.tsx | 26 ++ .../public/components/utility/index.ts | 10 + .../components/utility/with_suspense.tsx | 29 ++ src/plugins/shared_ux/public/index.ts | 4 + src/plugins/shared_ux/public/mocks/index.ts | 10 + src/plugins/shared_ux/public/plugin.tsx | 43 +++ .../shared_ux/public/services/index.tsx | 47 ++++ .../shared_ux/public/services/kibana/index.ts | 22 ++ .../public/services/kibana/platform.ts | 26 ++ .../shared_ux/public/services/mocks/index.ts | 20 ++ .../public/services/mocks/platform.mock.ts | 22 ++ .../shared_ux/public/services/platform.ts | 23 ++ .../public/services/storybook/index.ts | 18 ++ .../public/services/storybook/platform.ts | 24 ++ .../shared_ux/public/services/stub/index.ts | 18 ++ .../public/services/stub/platform.ts | 22 ++ .../shared_ux/public/services/types.ts | 42 +++ src/plugins/shared_ux/public/types.ts | 22 +- src/plugins/shared_ux/public/types/mdx.d.ts | 14 + 30 files changed, 1085 insertions(+), 5 deletions(-) create mode 100644 src/plugins/shared_ux/.storybook/decorators.tsx create mode 100644 src/plugins/shared_ux/.storybook/preview.ts create mode 100644 src/plugins/shared_ux/jest.config.js create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.component.tsx create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.mdx create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.stories.tsx create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.test.tsx create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.tsx create mode 100644 src/plugins/shared_ux/public/components/exit_full_screen_button/index.ts create mode 100644 src/plugins/shared_ux/public/components/index.ts create mode 100644 src/plugins/shared_ux/public/components/utility/fallback.tsx create mode 100644 src/plugins/shared_ux/public/components/utility/index.ts create mode 100644 src/plugins/shared_ux/public/components/utility/with_suspense.tsx create mode 100644 src/plugins/shared_ux/public/mocks/index.ts create mode 100755 src/plugins/shared_ux/public/plugin.tsx create mode 100644 src/plugins/shared_ux/public/services/index.tsx create mode 100644 src/plugins/shared_ux/public/services/kibana/index.ts create mode 100644 src/plugins/shared_ux/public/services/kibana/platform.ts create mode 100644 src/plugins/shared_ux/public/services/mocks/index.ts create mode 100644 src/plugins/shared_ux/public/services/mocks/platform.mock.ts create mode 100644 src/plugins/shared_ux/public/services/platform.ts create mode 100644 src/plugins/shared_ux/public/services/storybook/index.ts create mode 100644 src/plugins/shared_ux/public/services/storybook/platform.ts create mode 100644 src/plugins/shared_ux/public/services/stub/index.ts create mode 100644 src/plugins/shared_ux/public/services/stub/platform.ts create mode 100644 src/plugins/shared_ux/public/services/types.ts create mode 100644 src/plugins/shared_ux/public/types/mdx.d.ts diff --git a/src/plugins/shared_ux/.storybook/decorators.tsx b/src/plugins/shared_ux/.storybook/decorators.tsx new file mode 100644 index 0000000000000..c17af2cda0406 --- /dev/null +++ b/src/plugins/shared_ux/.storybook/decorators.tsx @@ -0,0 +1,20 @@ +/* + * 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 { DecoratorFn } from '@storybook/react'; +import { ServicesProvider } from '../public/services'; +import { servicesFactory } from '../public/services/storybook'; + +/** + * A Storybook decorator that provides the Shared UX `ServicesProvider` with Storybook-specific + * implementations to stories. + */ +export const servicesDecorator: DecoratorFn = (storyFn) => ( + {storyFn()} +); diff --git a/src/plugins/shared_ux/.storybook/preview.ts b/src/plugins/shared_ux/.storybook/preview.ts new file mode 100644 index 0000000000000..e4be2592482f3 --- /dev/null +++ b/src/plugins/shared_ux/.storybook/preview.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. + */ + +import { addDecorator } from '@storybook/react'; +import { servicesDecorator } from './decorators'; + +addDecorator(servicesDecorator); diff --git a/src/plugins/shared_ux/jest.config.js b/src/plugins/shared_ux/jest.config.js new file mode 100644 index 0000000000000..bc8d67e5ac35b --- /dev/null +++ b/src/plugins/shared_ux/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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: ['/src/plugins/shared_ux'], + transform: { + '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', + }, + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/shared_ux', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/shared_ux/{common,public,server}/**/*.{js,ts,tsx}'], +}; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap b/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap new file mode 100644 index 0000000000000..25dd2bb216a68 --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/__snapshots__/exit_full_screen_button.test.tsx.snap @@ -0,0 +1,249 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` is rendered 1`] = ` +.emotion-0.euiButton:hover, +.emotion-0.euiButton:focus, +.emotion-0.euiButton:focus-within { + -webkit-text-decoration: none; + text-decoration: none; +} + +.emotion-0.euiButton .euiButton__content { + padding: 0 8px; +} + +.emotion-0.euiButton--text { + background-color: #000; + color: #98a2b3; +} + +.emotion-0.euiButton--text:hover, +.emotion-0.euiButton--text:focus, +.emotion-0.euiButton--text:focus-within { + background-color: #000; + color: #f0f4fb; +} + + + + +
+ +

+ In full screen mode, press ESC to exit. +

+
+ + + + + + + +
+
+
+
+`; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.component.tsx b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.component.tsx new file mode 100644 index 0000000000000..d997c37262f7c --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.component.tsx @@ -0,0 +1,103 @@ +/* + * 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, HTMLAttributes } from 'react'; +import { + EuiScreenReaderOnly, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + useEuiTheme, + makeHighContrastColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; + +const label = i18n.translate('sharedUX.exitFullScreenButton.exitFullScreenModeButtonAriaLabel', { + defaultMessage: 'Exit full screen mode', +}); + +const text = i18n.translate('sharedUX.exitFullScreenButton.exitFullScreenModeButtonText', { + defaultMessage: 'Exit full screen', +}); + +const description = i18n.translate('sharedUX.exitFullScreenButton.fullScreenModeDescription', { + defaultMessage: 'In full screen mode, press ESC to exit.', +}); + +export interface Props extends Pick, 'className'> { + onClick: MouseEventHandler; +} + +/** + * A presentational component that renders a button designed to exit "full screen" mode. + */ +export const ExitFullScreenButton = ({ onClick, className }: Props) => { + const { euiTheme } = useEuiTheme(); + const { colors, size } = euiTheme; + + const textCSS = css` + ${makeHighContrastColor(colors.mediumShade)(colors.fullShade)}; + `; + + const buttonCSS = css` + &.euiButton { + &:hover, + &:focus, + &:focus-within { + text-decoration: none; + } + .euiButton__content { + padding: 0 ${size.s}; + } + } + + &.euiButton--text { + background-color: ${colors.fullShade}; + color: ${makeHighContrastColor(colors.mediumShade)(colors.fullShade)}; + + &:hover, + &:focus, + &:focus-within { + background-color: ${colors.fullShade}; + color: ${makeHighContrastColor(colors.lightestShade)(colors.fullShade)}; + } + } + `; + + return ( +
+ +

{description}

+
+ + + + + + + + {text} + + + + +
+ ); +}; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.mdx b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.mdx new file mode 100644 index 0000000000000..82f1eb0fde1ba --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.mdx @@ -0,0 +1,20 @@ +--- +id: sharedUX/Components/ExitFullScreenButton +slug: /shared-ux/components/exit-full-screen-button +title: Exit Full Screen Button +summary: A button that floats over the plugin workspace and allows one to exit "full screen" mode. +tags: ['shared-ux', 'component'] +date: 2021-12-28 +--- + +> This documentation is in-progress. + +When a plugin moves to "full screen" mode, the Kibana Chrome can be hidden entirely. This button floats over the plugin workspace and allows someone to exit full screen mode and restore the Kibana Chrome. + +The pure component, `exit_full_screen_button.tsx`, contains the base styles and behaviors. + +The connected component, `exit_full_screen_button.tsx`, uses services from the `shared_ux` plugin to show and hide the Kibana chrome. You must wrap your plugin app in the `ServicesContext` provided by the start contract of the `shared_ux` plugin to use it. + +This component is provided with `React.lazy` to avoid bundle bloat. + +This component is not currently eligible for promotion to EUI. diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.stories.tsx b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.stories.tsx new file mode 100644 index 0000000000000..e530a4303e6af --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.stories.tsx @@ -0,0 +1,40 @@ +/* + * 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 { action } from '@storybook/addon-actions'; + +import { ExitFullScreenButton as ExitFullScreenButtonComponent } from './exit_full_screen_button.component'; +import { ExitFullScreenButton } from './exit_full_screen_button'; +import mdx from './exit_full_screen_button.mdx'; + +export default { + title: 'Exit Full Screen Button', + description: + 'A button that floats over the plugin workspace and allows one to exit "full screen" mode.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const ConnectedComponent = ({ toggleChrome = true }: { toggleChrome: boolean }) => { + return ; +}; + +ConnectedComponent.argTypes = { + toggleChrome: { + control: 'boolean', + defaultValue: true, + }, +}; + +export const PureComponent = () => { + return ; +}; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.test.tsx b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.test.tsx new file mode 100644 index 0000000000000..dc634810a5794 --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { mount as enzymeMount, ReactWrapper } from 'enzyme'; +import { keys } from '@elastic/eui'; + +import { ServicesProvider, SharedUXServices } from '../../services'; +import { servicesFactory } from '../../services/mocks'; +import { ExitFullScreenButton } from './exit_full_screen_button'; + +describe('', () => { + let services: SharedUXServices; + let mount: (element: JSX.Element) => ReactWrapper; + + beforeEach(() => { + services = servicesFactory(); + mount = (element: JSX.Element) => + enzymeMount({element}); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('is rendered', () => { + const component = mount(); + + expect(component).toMatchSnapshot(); + }); + + test('passing `false` to toggleChrome does not toggle chrome', () => { + const component = mount(); + expect(services.platform.setIsFullscreen).toHaveBeenCalledTimes(0); + component.unmount(); + expect(services.platform.setIsFullscreen).toHaveBeenCalledTimes(0); + }); + + describe('onExit', () => { + const onExitHandler = jest.fn(); + let component: ReactWrapper; + + beforeEach(() => { + component = mount(); + }); + + test('is called when the button is pressed', () => { + expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(false); + + component.find('button').simulate('click'); + + expect(onExitHandler).toHaveBeenCalledTimes(1); + + component.unmount(); + + expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(true); + }); + + test('is called when the ESC key is pressed', () => { + expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(false); + + const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any); + document.dispatchEvent(escapeKeyEvent); + + expect(onExitHandler).toHaveBeenCalledTimes(1); + + component.unmount(); + + expect(services.platform.setIsFullscreen).toHaveBeenLastCalledWith(true); + }); + }); +}); diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.tsx new file mode 100644 index 0000000000000..5f54149b7cb55 --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/exit_full_screen_button.tsx @@ -0,0 +1,71 @@ +/* + * 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, useEffect } from 'react'; +import { useEuiTheme, keys } from '@elastic/eui'; +import { css } from '@emotion/react'; + +import { ExitFullScreenButton as Component } from './exit_full_screen_button.component'; +import { usePlatformService } from '../../services'; + +export interface Props { + onExit: () => void; + toggleChrome?: boolean; +} + +/** + * A service-enabled component that provides Kibana-specific functionality to the `ExitFullScreenButton` + * component. Use of this component requires both the `EuiTheme` context as well as the Shared UX + * `ServicesProvider`. + * + * See shared-ux/public/services for information. + */ +export const ExitFullScreenButton = ({ onExit, toggleChrome = false }: Props) => { + const { euiTheme } = useEuiTheme(); + const { setIsFullscreen } = usePlatformService(); + + const onClick = useCallback(() => { + if (toggleChrome) { + setIsFullscreen(true); + } + onExit(); + }, [onExit, setIsFullscreen, toggleChrome]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === keys.ESCAPE) { + onClick(); + } + }, + [onClick] + ); + + useEffect(() => { + document.addEventListener('keydown', onKeyDown, false); + + if (toggleChrome) { + setIsFullscreen(false); + } + + // cleanup the listener + return () => { + document.removeEventListener('keydown', onKeyDown, false); + }; + }, [onKeyDown, toggleChrome, setIsFullscreen]); + + // override the z-index: 1 applied to all non-eui elements that are in :focus via kui + // see packages/kbn-ui-framework/src/global_styling/reset/_reset.scss + const buttonCSS = css` + bottom: ${euiTheme.size.s}; + left: ${euiTheme.size.s}; + position: fixed; + z-index: 5; + `; + + return ; +}; diff --git a/src/plugins/shared_ux/public/components/exit_full_screen_button/index.ts b/src/plugins/shared_ux/public/components/exit_full_screen_button/index.ts new file mode 100644 index 0000000000000..b3450a87bb3c3 --- /dev/null +++ b/src/plugins/shared_ux/public/components/exit_full_screen_button/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { ExitFullScreenButton } from './exit_full_screen_button'; +export { ExitFullScreenButton } from './exit_full_screen_button'; + +// React.lazy requires default export +// eslint-disable-next-line import/no-default-export +export default ExitFullScreenButton; diff --git a/src/plugins/shared_ux/public/components/index.ts b/src/plugins/shared_ux/public/components/index.ts new file mode 100644 index 0000000000000..f3c25ca023e8d --- /dev/null +++ b/src/plugins/shared_ux/public/components/index.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 React from 'react'; +import { withSuspense } from './utility'; + +/** + * The Lazily-loaded `ExitFullScreenButton` component. Consumers should use `React.Suspennse` or the + * `withSuspense` HOC to load this component. + */ +export const LazyExitFullScreenButton = React.lazy(() => import('./exit_full_screen_button')); + +/** + * A `ExitFullScreenButton` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `LazyExitFullScreenButton` component lazily with + * a predefined fallback and error boundary. + */ +export const ExitFullScreenButton = withSuspense(LazyExitFullScreenButton); diff --git a/src/plugins/shared_ux/public/components/utility/fallback.tsx b/src/plugins/shared_ux/public/components/utility/fallback.tsx new file mode 100644 index 0000000000000..721f562d33040 --- /dev/null +++ b/src/plugins/shared_ux/public/components/utility/fallback.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { css } from '@emotion/react'; + +/** + * A simple implementation of `React.Suspense.fallback` that renders a loading spinner. + */ +export const Fallback = () => { + const divCSS = css` + text-align: center; + `; + + return ( +
+ +
+ ); +}; diff --git a/src/plugins/shared_ux/public/components/utility/index.ts b/src/plugins/shared_ux/public/components/utility/index.ts new file mode 100644 index 0000000000000..4e930810a6895 --- /dev/null +++ b/src/plugins/shared_ux/public/components/utility/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { Fallback } from './fallback'; +export { withSuspense } from './with_suspense'; diff --git a/src/plugins/shared_ux/public/components/utility/with_suspense.tsx b/src/plugins/shared_ux/public/components/utility/with_suspense.tsx new file mode 100644 index 0000000000000..cd9a02c2d6bb4 --- /dev/null +++ b/src/plugins/shared_ux/public/components/utility/with_suspense.tsx @@ -0,0 +1,29 @@ +/* + * 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, { Suspense, ComponentType, ReactElement, Ref } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; + +import { Fallback } from './fallback'; + +/** + * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors. + * @param Component A component deferred by `React.lazy` + * @param fallback A fallback component to render while things load; default is `Fallback` from SharedUX. + */ +export const withSuspense =

( + Component: ComponentType

, + fallback: ReactElement | null = +) => + React.forwardRef((props: P, ref: Ref) => ( + + + + + + )); diff --git a/src/plugins/shared_ux/public/index.ts b/src/plugins/shared_ux/public/index.ts index f68c6d148011e..e6a2f925c5120 100755 --- a/src/plugins/shared_ux/public/index.ts +++ b/src/plugins/shared_ux/public/index.ts @@ -8,8 +8,12 @@ import { SharedUXPlugin } from './plugin'; +/** + * Creates the Shared UX plugin. + */ export function plugin() { return new SharedUXPlugin(); } export type { SharedUXPluginSetup, SharedUXPluginStart } from './types'; +export { ExitFullScreenButton, LazyExitFullScreenButton } from './components'; diff --git a/src/plugins/shared_ux/public/mocks/index.ts b/src/plugins/shared_ux/public/mocks/index.ts new file mode 100644 index 0000000000000..31227c685fdec --- /dev/null +++ b/src/plugins/shared_ux/public/mocks/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { MockPlatformServiceFactory } from '../services/mocks'; +export { platformServiceFactory, servicesFactory } from '../services/mocks'; diff --git a/src/plugins/shared_ux/public/plugin.tsx b/src/plugins/shared_ux/public/plugin.tsx new file mode 100755 index 0000000000000..1d2840566d4d9 --- /dev/null +++ b/src/plugins/shared_ux/public/plugin.tsx @@ -0,0 +1,43 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { + SharedUXPluginSetup, + SharedUXPluginStart, + SharedUXPluginStartDeps, + SharedUXPluginSetupDeps, +} from './types'; + +import { ServicesProvider } from './services'; +import { servicesFactory } from './services/kibana'; + +/** + * The Kibana plugin for Shared User Experience (Shared UX). + */ +export class SharedUXPlugin implements Plugin { + public setup( + _coreSetup: CoreSetup, + _setupPlugins: SharedUXPluginSetupDeps + ): SharedUXPluginSetup { + return {}; + } + + public start(coreStart: CoreStart, startPlugins: SharedUXPluginStartDeps): SharedUXPluginStart { + const services = servicesFactory({ coreStart, startPlugins }); + + return { + ServicesContext: ({ children }) => ( + {children} + ), + }; + } + + public stop() {} +} diff --git a/src/plugins/shared_ux/public/services/index.tsx b/src/plugins/shared_ux/public/services/index.tsx new file mode 100644 index 0000000000000..acc8b9294d1df --- /dev/null +++ b/src/plugins/shared_ux/public/services/index.tsx @@ -0,0 +1,47 @@ +/* + * 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, createContext, useContext } from 'react'; +import { SharedUXPlatformService } from './platform'; +import { servicesFactory } from './stub'; + +/** + * A collection of services utilized by SharedUX. This serves as a thin + * abstraction layer between services provided by Kibana and other plugins + * while allowing this plugin to be developed independently of those contracts. + * + * It also allows us to "swap out" differenct implementations of these services + * for different environments, (e.g. Jest, Storybook, etc.) + */ +export interface SharedUXServices { + platform: SharedUXPlatformService; +} + +// The React Context used to provide the services to the SharedUX components. +const ServicesContext = createContext(servicesFactory()); + +/** + * The `React.Context` Provider component for the `SharedUXServices` context. Any + * plugin or environemnt that consumes SharedUX components needs to wrap their React + * tree with this provider. + */ +export const ServicesProvider: FC = ({ children, ...services }) => ( + {children} +); + +/** + * React hook for accessing the pre-wired `SharedUXServices`. + */ +export function useServices() { + return useContext(ServicesContext); +} + +/** + * React hook for accessing the pre-wired `SharedUXPlatformService`. + */ +export const usePlatformService = () => useServices().platform; diff --git a/src/plugins/shared_ux/public/services/kibana/index.ts b/src/plugins/shared_ux/public/services/kibana/index.ts new file mode 100644 index 0000000000000..f7c4cd7b2c56d --- /dev/null +++ b/src/plugins/shared_ux/public/services/kibana/index.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. + */ + +import type { SharedUXServices } from '..'; +import type { SharedUXPluginStartDeps } from '../../types'; +import type { KibanaPluginServiceFactory } from '../types'; +import { platformServiceFactory } from './platform'; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXServices`. + */ +export const servicesFactory: KibanaPluginServiceFactory< + SharedUXServices, + SharedUXPluginStartDeps +> = (params) => ({ + platform: platformServiceFactory(params), +}); diff --git a/src/plugins/shared_ux/public/services/kibana/platform.ts b/src/plugins/shared_ux/public/services/kibana/platform.ts new file mode 100644 index 0000000000000..9872149ee02f2 --- /dev/null +++ b/src/plugins/shared_ux/public/services/kibana/platform.ts @@ -0,0 +1,26 @@ +/* + * 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 { SharedUXPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../types'; +import { SharedUXPlatformService } from '../platform'; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXPlatformService`. + */ +export type PlatformServiceFactory = KibanaPluginServiceFactory< + SharedUXPlatformService, + SharedUXPluginStartDeps +>; + +/** + * A factory function for creating a Kibana-based implementation of `SharedUXPlatformService`. + */ +export const platformServiceFactory: PlatformServiceFactory = ({ coreStart }) => ({ + setIsFullscreen: (isVisible: boolean) => coreStart.chrome.setIsVisible(isVisible), +}); diff --git a/src/plugins/shared_ux/public/services/mocks/index.ts b/src/plugins/shared_ux/public/services/mocks/index.ts new file mode 100644 index 0000000000000..a7046bb78fa3e --- /dev/null +++ b/src/plugins/shared_ux/public/services/mocks/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { MockPlatformServiceFactory, platformServiceFactory } from './platform.mock'; + +import type { SharedUXServices } from '../.'; +import { PluginServiceFactory } from '../types'; +import { platformServiceFactory } from './platform.mock'; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXServices`. + */ +export const servicesFactory: PluginServiceFactory = () => ({ + platform: platformServiceFactory(), +}); diff --git a/src/plugins/shared_ux/public/services/mocks/platform.mock.ts b/src/plugins/shared_ux/public/services/mocks/platform.mock.ts new file mode 100644 index 0000000000000..c36d63cfcacbe --- /dev/null +++ b/src/plugins/shared_ux/public/services/mocks/platform.mock.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. + */ + +import type { PluginServiceFactory } from '../types'; +import type { SharedUXPlatformService } from '../platform'; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXPlatformService`. + */ +export type MockPlatformServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Jest-based implementation of `SharedUXPlatformService`. + */ +export const platformServiceFactory: MockPlatformServiceFactory = () => ({ + setIsFullscreen: jest.fn(), +}); diff --git a/src/plugins/shared_ux/public/services/platform.ts b/src/plugins/shared_ux/public/services/platform.ts new file mode 100644 index 0000000000000..52f88a1133c8b --- /dev/null +++ b/src/plugins/shared_ux/public/services/platform.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. + */ + +/** + * A services providing methods to interact with the Platform in which this plugin is + * running, (almost always Kibana). + * + * Rather than provide the entire `CoreStart` contract to components, we provide simplified + * abstractions around a use case specific to Shared UX. This way, we know exactly how the + * `CoreStart` and other plugins are used, like specifically which methods. This makes + * mocking and refactoring easier when upstream dependencies change. + */ +export interface SharedUXPlatformService { + /** + * Sets the fullscreen state of the chrome. + */ + setIsFullscreen: (isFullscreen: boolean) => void; +} diff --git a/src/plugins/shared_ux/public/services/storybook/index.ts b/src/plugins/shared_ux/public/services/storybook/index.ts new file mode 100644 index 0000000000000..8ea03317e53a2 --- /dev/null +++ b/src/plugins/shared_ux/public/services/storybook/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { SharedUXServices } from '../.'; +import { PluginServiceFactory } from '../types'; +import { platformServiceFactory } from './platform'; + +/** + * A factory function for creating a Storybook-based implementation of `SharedUXServices`. + */ +export const servicesFactory: PluginServiceFactory = (params) => ({ + platform: platformServiceFactory(params), +}); diff --git a/src/plugins/shared_ux/public/services/storybook/platform.ts b/src/plugins/shared_ux/public/services/storybook/platform.ts new file mode 100644 index 0000000000000..8d7645ee441ca --- /dev/null +++ b/src/plugins/shared_ux/public/services/storybook/platform.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 { action } from '@storybook/addon-actions'; + +import { PluginServiceFactory } from '../types'; +import { SharedUXPlatformService } from '../platform'; + +/** + * A factory function for creating a Storybook-based implementation of `SharedUXPlatformService`. + */ +export type PlatformServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a Storybook-based implementation of `SharedUXPlatformService`. + */ +export const platformServiceFactory: PlatformServiceFactory = () => ({ + setIsFullscreen: action('setIsChromeVisible'), +}); diff --git a/src/plugins/shared_ux/public/services/stub/index.ts b/src/plugins/shared_ux/public/services/stub/index.ts new file mode 100644 index 0000000000000..8a5afe8cdcafb --- /dev/null +++ b/src/plugins/shared_ux/public/services/stub/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { SharedUXServices } from '../.'; +import { PluginServiceFactory } from '../types'; +import { platformServiceFactory } from './platform'; + +/** + * A factory function for creating a simple stubbed implemetation of `SharedUXServices`. + */ +export const servicesFactory: PluginServiceFactory = () => ({ + platform: platformServiceFactory(), +}); diff --git a/src/plugins/shared_ux/public/services/stub/platform.ts b/src/plugins/shared_ux/public/services/stub/platform.ts new file mode 100644 index 0000000000000..90fa8edb3e06e --- /dev/null +++ b/src/plugins/shared_ux/public/services/stub/platform.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. + */ + +import { PluginServiceFactory } from '../types'; +import { SharedUXPlatformService } from '../platform'; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXPlatformService`. + */ +export type PlatformServiceFactory = PluginServiceFactory; + +/** + * A factory function for creating a simple stubbed implementation of `SharedUXPlatformService`. + */ +export const platformServiceFactory: PlatformServiceFactory = () => ({ + setIsFullscreen: (_isFullscreen) => {}, +}); diff --git a/src/plugins/shared_ux/public/services/types.ts b/src/plugins/shared_ux/public/services/types.ts new file mode 100644 index 0000000000000..2645d5303a33b --- /dev/null +++ b/src/plugins/shared_ux/public/services/types.ts @@ -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 { BehaviorSubject } from 'rxjs'; +import { CoreStart, AppUpdater, PluginInitializerContext } from 'src/core/public'; + +/** + * A factory function for creating one or more services. + * + * The `S` generic determines the shape of the API being produced. + * The `Parameters` generic determines what parameters are expected to + * create the service. + */ +export type PluginServiceFactory = (params: Parameters) => S; + +/** + * Parameters necessary to create a Kibana-based service, (e.g. during Plugin + * startup or setup). + * + * The `Start` generic refers to the specific Plugin `TPluginsStart`. + */ +export interface KibanaPluginServiceParams { + coreStart: CoreStart; + startPlugins: Start; + appUpdater?: BehaviorSubject; + initContext?: PluginInitializerContext; +} + +/** + * A factory function for creating a Kibana-based service. + * + * The `Service` generic determines the shape of the Service being produced. + * The `Start` generic refers to the specific Plugin `TPluginsStart`. + */ +export type KibanaPluginServiceFactory = ( + params: KibanaPluginServiceParams +) => Service; diff --git a/src/plugins/shared_ux/public/types.ts b/src/plugins/shared_ux/public/types.ts index c27cba3a866ca..12ded4cde79b8 100755 --- a/src/plugins/shared_ux/public/types.ts +++ b/src/plugins/shared_ux/public/types.ts @@ -6,14 +6,26 @@ * Side Public License, v 1. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { FC } from 'react'; + +/** @internal */ export interface SharedUXPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SharedUXPluginStart {} +/** + * The Shared UX plugin public contract, containing prewired components, services, and + * other constructs useful to consumers. + */ +export interface SharedUXPluginStart { + /** + * A React component that provides a pre-wired `React.Context` which connects components to Shared UX services. + */ + ServicesContext: FC; +} -// eslint-disable-next-line @typescript-eslint/no-empty-interface +/** @internal */ export interface SharedUXPluginSetupDeps {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface +/** @internal */ export interface SharedUXPluginStartDeps {} diff --git a/src/plugins/shared_ux/public/types/mdx.d.ts b/src/plugins/shared_ux/public/types/mdx.d.ts new file mode 100644 index 0000000000000..546349e87a9c3 --- /dev/null +++ b/src/plugins/shared_ux/public/types/mdx.d.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +// Importing MDX files requires a type definition not currently included in the stack. +declare module '*.mdx' { + let MDXComponent: (props) => JSX.Element; + // eslint-disable-next-line import/no-default-export + export default MDXComponent; +}