diff --git a/CHANGELOG.md b/CHANGELOG.md index 6539fbcdc3d8..82d1175a15c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) - [Multiple Datasource] Add component to show single selected data source in read only mode ([#6125](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) - [Workspace] Add workspace id in basePath ([#6060](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6060)) +- Add sidecar service ([#5920](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5920)) ### 🐛 Bug Fixes diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index be879bb4b5e9..154f07caf46c 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -41,6 +41,7 @@ import { notificationServiceMock } from '../notifications/notifications_service. import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { ChromeService } from './chrome_service'; import { getAppInfo } from '../application/utils'; +import { overlayServiceMock } from '../mocks'; class FakeApp implements App { public title: string; @@ -70,6 +71,7 @@ function defaultStartDeps(availableApps?: App[]) { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + overlays: overlayServiceMock.createStartContract(), }; if (availableApps) { 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/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 8d244a212d1f..790f24bc20e9 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1927,6 +1927,58 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + sidecarConfig$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "dockedMode": "right", + "paddingSize": 640, + }, + "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, + } + } survey="/" >
({ htmlIdGenerator: () => () => 'mockId', @@ -72,6 +73,10 @@ function mockProps() { branding: {}, survey: '/', logos: chromeServiceMock.createStartContract().logos, + sidecarConfig$: new BehaviorSubject({ + dockedMode: SIDECAR_DOCKED_MODE.RIGHT, + paddingSize: 640, + }), }; } 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 overlayStart = overlayModalServiceMock.createStartContract(); @@ -41,6 +42,7 @@ const createStartContractMock = () => { openModal: overlayStart.open, openConfirm: overlayStart.openConfirm, banners: overlayBannersServiceMock.createStartContract(), + sidecar: overlaySidecarServiceMock.createStartContract(), }; return startContract; }; diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts index 19d5d6b8b7c6..92144d34c45b 100644 --- a/src/core/public/overlays/overlay_service.ts +++ b/src/core/public/overlays/overlay_service.ts @@ -33,6 +33,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { OverlayBannersStart, OverlayBannersService } from './banners'; import { FlyoutService, OverlayFlyoutStart } from './flyout'; import { ModalService, OverlayModalStart } from './modal'; +import { SidecarService, OverlaySidecarStart } from './sidecar'; interface StartDeps { i18n: I18nStart; @@ -45,6 +46,7 @@ export class OverlayService { private bannersService = new OverlayBannersService(); private modalService = new ModalService(); private flyoutService = new FlyoutService(); + private sidecarService = new SidecarService(); public start({ i18n, targetDomElement, uiSettings }: StartDeps): OverlayStart { const flyoutElement = document.createElement('div'); @@ -56,12 +58,19 @@ export class OverlayService { const modalElement = document.createElement('div'); targetDomElement.appendChild(modalElement); const modals = this.modalService.start({ i18n, targetDomElement: modalElement }); + const sidecarElement = document.createElement('div'); + targetDomElement.appendChild(sidecarElement); + const sidecars = this.sidecarService.start({ + i18n, + targetDomElement: sidecarElement, + }); return { banners, openFlyout: flyouts.open.bind(flyouts), openModal: modals.open.bind(modals), openConfirm: modals.openConfirm.bind(modals), + sidecar: sidecars, }; } } @@ -76,4 +85,6 @@ export interface OverlayStart { openModal: OverlayModalStart['open']; /** {@link OverlayModalStart#openConfirm} */ openConfirm: OverlayModalStart['openConfirm']; + /** {@link OverlaySidecarStart#open} */ + sidecar: OverlaySidecarStart; } diff --git a/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap b/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap new file mode 100644 index 000000000000..95d617b3acfe --- /dev/null +++ b/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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#close() can be called multiple times on the same SidecarRef 1`] = ` +Array [ + Array [ +
, + ], +] +`; + +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 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..9407e6c6956b --- /dev/null +++ b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`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, + } + } +> + +
+ +