From a4fadfc99042bc51df2e7564d80793f506feb804 Mon Sep 17 00:00:00 2001 From: tygao Date: Thu, 11 Jan 2024 15:52:34 +0800 Subject: [PATCH 01/20] feat: sidecar poc Signed-off-by: tygao --- src/core/public/chrome/chrome_service.tsx | 5 + src/core/public/chrome/ui/header/header.tsx | 13 +- src/core/public/core_system.ts | 1 + src/core/public/overlays/index.ts | 6 + src/core/public/overlays/overlay_service.ts | 11 + src/core/public/overlays/sidecar/helper.ts | 30 +++ src/core/public/overlays/sidecar/index.ts | 12 + .../overlays/sidecar/resizable_button.tsx | 84 +++++++ .../overlays/sidecar/rsizable_button.scss | 131 ++++++++++ .../overlays/sidecar/sidecar_service.scss | 40 +++ .../overlays/sidecar/sidecar_service.tsx | 232 ++++++++++++++++++ src/core/public/rendering/app_containers.tsx | 18 +- .../public/rendering/rendering_service.tsx | 5 +- .../public/overlays/create_react_overlays.tsx | 6 + .../public/overlays/types.ts | 1 + 15 files changed, 588 insertions(+), 7 deletions(-) create mode 100644 src/core/public/overlays/sidecar/helper.ts create mode 100644 src/core/public/overlays/sidecar/index.ts create mode 100644 src/core/public/overlays/sidecar/resizable_button.tsx create mode 100644 src/core/public/overlays/sidecar/rsizable_button.scss create mode 100644 src/core/public/overlays/sidecar/sidecar_service.scss create mode 100644 src/core/public/overlays/sidecar/sidecar_service.tsx diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 57c9f11d9061..a6f1f6ab317e 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -51,6 +51,7 @@ import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; import { Branding } from '../'; import { getLogos } from '../../common'; import type { Logos } from '../../common/types'; +import { OverlayStart } from '../overlays'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; @@ -96,6 +97,7 @@ export interface StartDeps { injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; + overlays: OverlayStart; } type CollapsibleNavHeaderRender = () => JSX.Element | null; @@ -166,6 +168,7 @@ export class ChromeService { injectedMetadata, notifications, uiSettings, + overlays, }: StartDeps): Promise { this.initVisibility(application); @@ -177,6 +180,7 @@ export class ChromeService { const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const sidecarConfig$ = overlays.sidecar.getSidecarConfig$(); const navControls = this.navControls.start(); const navLinks = this.navLinks.start({ application, http }); @@ -280,6 +284,7 @@ export class ChromeService { logos={logos} survey={injectedMetadata.getSurvey()} collapsibleNavHeaderRender={this.collapsibleNavHeaderRender} + sidecarConfig$={sidecarConfig$} /> ), diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 2ca0f2548942..b8b40fa6c39f 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -41,7 +41,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import classnames from 'classnames'; -import React, { createRef, useState } from 'react'; +import React, { createRef, useMemo, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import { LoadingIndicator } from '../'; @@ -65,7 +65,7 @@ import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; import { HeaderLogo } from './header_logo'; import type { Logos } from '../../../../common/types'; - +import { ISidecarConfig, getOsdSidecarPaddingStyle } from '../../../overlays'; export interface HeaderProps { opensearchDashboardsVersion: string; application: InternalApplicationStart; @@ -94,6 +94,7 @@ export interface HeaderProps { branding: ChromeBranding; logos: Logos; survey: string | undefined; + sidecarConfig$: Observable; } export function Header({ @@ -112,6 +113,11 @@ export function Header({ const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); const [isNavOpen, setIsNavOpen] = useState(false); + const sidecarConfig = useObservable(observables.sidecarConfig$, undefined); + + const sidecarPaddingStyle = useMemo(() => { + return getOsdSidecarPaddingStyle(sidecarConfig); + }, [sidecarConfig]); if (!isVisible) { return ; @@ -132,6 +138,7 @@ export function Header({ )} - + { + const clientX = isMouseEvent(event) ? event.clientX : event.touches[0].clientX; + const clientY = isMouseEvent(event) ? event.clientY : event.touches[0].clientY; + return isHorizontal ? clientX : clientY; +}; + +export const getOsdSidecarPaddingStyle = (config: ISidecarConfig | undefined) => { + if ( + !config?.isHidden && + (config?.dockedDirection === 'left' || config?.dockedDirection === 'right') + ) { + const { dockedDirection, paddingSize } = config; + return { + [`padding-${dockedDirection}`]: paddingSize, + }; + } + return {}; +}; diff --git a/src/core/public/overlays/sidecar/index.ts b/src/core/public/overlays/sidecar/index.ts new file mode 100644 index 000000000000..5963bd402469 --- /dev/null +++ b/src/core/public/overlays/sidecar/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { + SidecarService, + OverlaySidecarStart, + OverlaySidecarOpenOptions, + ISidecarConfig, +} from './sidecar_service'; +export { getOsdSidecarPaddingStyle } from './helper'; diff --git a/src/core/public/overlays/sidecar/resizable_button.tsx b/src/core/public/overlays/sidecar/resizable_button.tsx new file mode 100644 index 000000000000..ec8426aae3d6 --- /dev/null +++ b/src/core/public/overlays/sidecar/resizable_button.tsx @@ -0,0 +1,84 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { MouseEvent, useCallback, TouchEvent, useRef } from 'react'; +import classNames from 'classnames'; +import './rsizable_button.scss'; +import { getPosition } from './helper'; +import { ISidecarConfig } from './sidecar_service'; + +interface Props { + isHorizontal: boolean; + onResize: (size: number) => void; + flyoutSize: number; + dockedDirection: ISidecarConfig['dockedDirection'] | undefined; + minSize?: number; +} + +const MIN_SIDECAR_SIZE = 200; + +export const ResizableButton = ({ + dockedDirection, + onResize, + flyoutSize, + minSize = MIN_SIDECAR_SIZE, +}: Props) => { + const isHorizontal = dockedDirection !== 'bottom'; + + const classes = classNames('resizableButton', { + 'resizableButton--vertical': !isHorizontal, + 'resizableButton--horizontal': isHorizontal, + }); + + const initialMouseXorY = useRef(0); + const initialFlyoutSize = useRef(flyoutSize); + const setFocus = (e: MouseEvent) => e.currentTarget.focus(); + + const onMouseMove = useCallback( + (event) => { + let offset; + if (dockedDirection === 'left') { + offset = getPosition(event, isHorizontal) - initialMouseXorY.current; + } else { + offset = initialMouseXorY.current - getPosition(event, isHorizontal); + } + const newFlyoutSize = initialFlyoutSize.current + offset; + + onResize(Math.max(newFlyoutSize, minSize)); + }, + [isHorizontal, dockedDirection, minSize, onResize] + ); + + const onMouseUp = useCallback(() => { + initialMouseXorY.current = 0; + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + window.removeEventListener('touchmove', onMouseMove); + window.removeEventListener('touchend', onMouseUp); + }, [onMouseMove]); + + const onMouseDown = useCallback( + (event: MouseEvent | TouchEvent) => { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + window.addEventListener('touchmove', onMouseMove); + window.addEventListener('touchend', onMouseUp); + initialMouseXorY.current = getPosition(event, isHorizontal); + initialFlyoutSize.current = flyoutSize; + }, + [isHorizontal, flyoutSize, onMouseMove, onMouseUp] + ); + + return ( +
Sidecar content
"`; + +exports[`SidecarService openSidecar() with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; diff --git a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap new file mode 100644 index 000000000000..9d46091330c3 --- /dev/null +++ b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`is rendered 1`] = ` +
Sidecar content
"`; + +exports[`SidecarService SidecarRef#Show() recover sidecar when calling show after calling hide 1`] = `"
Sidecar content
"`; + exports[`SidecarService SidecarRef#close() can be called multiple times on the same SidecarRef 1`] = ` Array [ Array [ @@ -8,6 +12,6 @@ Array [ ] `; -exports[`SidecarService openSidecar() renders a sidecar to the DOM 1`] = `"
Sidecar content
"`; +exports[`SidecarService open sidecar renders a sidecar to the DOM 1`] = `"
Sidecar content
"`; -exports[`SidecarService openSidecar() with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; +exports[`SidecarService open sidecar with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; diff --git a/src/core/public/overlays/sidecar/components/__snapshots__/sidecar.test.tsx.snap b/src/core/public/overlays/sidecar/components/__snapshots__/sidecar.test.tsx.snap new file mode 100644 index 000000000000..59d236eaa821 --- /dev/null +++ b/src/core/public/overlays/sidecar/components/__snapshots__/sidecar.test.tsx.snap @@ -0,0 +1,1594 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sidecar component is rendered 1`] = ` +
+
, + }, + Object {}, + Object { + "enqueueForceUpdate": [Function], + "enqueueReplaceState": [Function], + "enqueueSetState": [Function], + "isMounted": [Function], + }, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + ], + }, + } + } + mount={[Function]} + options={ + Object { + "data-test-subj": "sidecar-component-wrapper", + } + } + setSidecarConfig={[MockFunction]} + sidecarConfig$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "dockedMode": "right", + "isHidden": false, + "paddingSize": 460, + }, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } +> + +
+ +
, + }, + Object {}, + Object { + "enqueueForceUpdate": [Function], + "enqueueReplaceState": [Function], + "enqueueSetState": [Function], + "isMounted": [Function], + }, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + ], + }, + } + } + mount={[Function]} + options={ + Object { + "data-test-subj": "sidecar-component-wrapper", + } + } + setSidecarConfig={[MockFunction]} + sidecarConfig$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "dockedMode": "takeover", + "isHidden": false, + "paddingSize": 460, + }, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } +> + +
+ +
, + }, + Object {}, + Object { + "enqueueForceUpdate": [Function], + "enqueueReplaceState": [Function], + "enqueueSetState": [Function], + "isMounted": [Function], + }, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + ], + }, + } + } + mount={[Function]} + options={ + Object { + "data-test-subj": "sidecar-component-wrapper", + } + } + setSidecarConfig={[MockFunction]} + sidecarConfig$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "dockedMode": "takeover", + "isHidden": true, + "paddingSize": 460, + }, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } +> + +
+ +
, + }, + Object {}, + Object { + "enqueueForceUpdate": [Function], + "enqueueReplaceState": [Function], + "enqueueSetState": [Function], + "isMounted": [Function], + }, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + Array [ + Object { + "children":
+ + +
, + }, + Object {}, + ], + ], + "results": Array [ + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + Object { + "type": "return", + "value":
+ + +
, + }, + ], + }, + } + } + mount={[Function]} + options={ + Object { + "data-test-subj": "sidecar-component-wrapper", + } + } + setSidecarConfig={[MockFunction]} + sidecarConfig$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "dockedMode": "right", + "isHidden": false, + "paddingSize": 460, + }, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } +> + +
+ +
Sidecar content
"`; + exports[`SidecarService open sidecar renders a sidecar to the DOM 1`] = `"
Sidecar content
"`; exports[`SidecarService open sidecar with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; diff --git a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap index 9d46091330c3..8acfbacadd5c 100644 --- a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap +++ b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap @@ -44,7 +44,18 @@ exports[`it should emit onResize with min size when drag if new size is below th /> `; -exports[`it should emit onResize with new flyout size when drag and horizontal 1`] = ` +exports[`it should emit onResize with new flyout size when docked left and drag and horizontal 1`] = ` +
Sidecar content
"`; +exports[`SidecarService SidecarRef#Hide() hide the sidecar when calling hide 1`] = `"
Sidecar content
"`; -exports[`SidecarService SidecarRef#Show() recover sidecar when calling show after calling hide 1`] = `"
Sidecar content
"`; +exports[`SidecarService SidecarRef#Show() recover sidecar when calling show after calling hide 1`] = `"
Sidecar content
"`; exports[`SidecarService SidecarRef#close() can be called multiple times on the same SidecarRef 1`] = ` Array [ @@ -12,8 +12,8 @@ Array [ ] `; -exports[`SidecarService open sidecar does not unmount if targetDom is null 1`] = `"
Sidecar content
"`; +exports[`SidecarService open sidecar does not unmount if targetDom is null 1`] = `"
Sidecar content
"`; -exports[`SidecarService open sidecar renders a sidecar to the DOM 1`] = `"
Sidecar content
"`; +exports[`SidecarService open sidecar renders a sidecar to the DOM 1`] = `"
Sidecar content
"`; -exports[`SidecarService open sidecar with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; +exports[`SidecarService open sidecar with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`; diff --git a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap index 8acfbacadd5c..9407e6c6956b 100644 --- a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap +++ b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap @@ -2,7 +2,7 @@ exports[`is rendered 1`] = `