From 11a5ad458b48983eadf2cde616b028ea062d00aa Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Wed, 7 Jun 2023 14:31:20 +0800 Subject: [PATCH 01/54] setup workspace plugin project skeleton Signed-off-by: Yulong Ruan --- src/plugins/workspace/common/constants.ts | 2 + .../workspace/opensearch_dashboards.json | 10 ++++ src/plugins/workspace/public/application.tsx | 32 ++++++++++ .../workspace/public/components/routes.ts | 20 +++++++ .../public/components/workspace_app.tsx | 59 +++++++++++++++++++ .../components/workspace_creator/index.tsx | 5 ++ src/plugins/workspace/public/index.ts | 5 ++ src/plugins/workspace/public/plugin.ts | 38 ++++++++++++ 8 files changed, 171 insertions(+) create mode 100644 src/plugins/workspace/common/constants.ts create mode 100644 src/plugins/workspace/opensearch_dashboards.json create mode 100644 src/plugins/workspace/public/application.tsx create mode 100644 src/plugins/workspace/public/components/routes.ts create mode 100644 src/plugins/workspace/public/components/workspace_app.tsx create mode 100644 src/plugins/workspace/public/components/workspace_creator/index.tsx create mode 100644 src/plugins/workspace/public/index.ts create mode 100644 src/plugins/workspace/public/plugin.ts diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts new file mode 100644 index 000000000000..306fc5f3bef8 --- /dev/null +++ b/src/plugins/workspace/common/constants.ts @@ -0,0 +1,2 @@ +export const WORKSPACE_APP_ID = 'workspace'; +export const WORKSPACE_APP_NAME = 'Workspace'; diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json new file mode 100644 index 000000000000..0273a579bfcb --- /dev/null +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -0,0 +1,10 @@ +{ + "id": "workspace", + "version": "opensearchDashboards", + "server": false, + "ui": true, + "requiredPlugins": ["savedObjects"], + "requiredBundles": [ + "opensearchDashboardsReact" + ] +} diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx new file mode 100644 index 000000000000..b005aa0e87ad --- /dev/null +++ b/src/plugins/workspace/public/application.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from 'react-router-dom'; + +import { AppMountParameters, CoreStart } from '../../../core/public'; +import { OpenSearchDashboardsContextProvider } from '../../../plugins/opensearch_dashboards_react/public'; +import { WorkspaceApp } from './components/workspace_app'; + +interface Service extends CoreStart {} + +export const renderApp = ( + { element, history, appBasePath }: AppMountParameters, + services: Service +) => { + ReactDOM.render( + + + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/workspace/public/components/routes.ts b/src/plugins/workspace/public/components/routes.ts new file mode 100644 index 000000000000..5155fa9a1146 --- /dev/null +++ b/src/plugins/workspace/public/components/routes.ts @@ -0,0 +1,20 @@ +import { WorkspaceCreator } from './workspace_creator'; + +export const paths = { + create: '/create', +}; + +interface RouteConfig { + path: string; + Component: React.ComponentType; + label: string; + exact?: boolean; +} + +export const ROUTES: RouteConfig[] = [ + { + path: paths.create, + Component: WorkspaceCreator, + label: 'Create', + }, +]; diff --git a/src/plugins/workspace/public/components/workspace_app.tsx b/src/plugins/workspace/public/components/workspace_app.tsx new file mode 100644 index 000000000000..9b3e7974039d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_app.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import { EuiPage, EuiPageBody } from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { matchPath, Route, Switch, useLocation } from 'react-router-dom'; + +import { ROUTES } from './routes'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { ChromeBreadcrumb } from '../../../../core/public'; +import { WORKSPACE_APP_NAME } from '../../common/constants'; + +export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { + const { + services: { chrome }, + } = useOpenSearchDashboards(); + const location = useLocation(); + + /** + * map the current pathname to breadcrumbs + */ + useEffect(() => { + let pathname = location.pathname; + const breadcrumbs: ChromeBreadcrumb[] = []; + + while (pathname !== '/') { + const matchedRoute = ROUTES.find((route) => + matchPath(pathname, { path: route.path, exact: true }) + ); + if (matchedRoute) { + if (breadcrumbs.length === 0) { + breadcrumbs.unshift({ text: matchedRoute.label }); + } else { + breadcrumbs.unshift({ + text: matchedRoute.label, + href: `${appBasePath}${matchedRoute.path}`, + }); + } + } + const pathArr = pathname.split('/'); + pathArr.pop(); + pathname = pathArr.join('/') ? pathArr.join('/') : '/'; + } + breadcrumbs.unshift({ text: WORKSPACE_APP_NAME, href: appBasePath }); + chrome?.setBreadcrumbs(breadcrumbs); + }, [appBasePath, location.pathname, chrome?.setBreadcrumbs]); + + return ( + + + + + {ROUTES.map(({ path, Component, exact }) => ( + } exact={exact ?? false} /> + ))} + + + + + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_creator/index.tsx b/src/plugins/workspace/public/components/workspace_creator/index.tsx new file mode 100644 index 000000000000..0a1601db8966 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const WorkspaceCreator = () => { + return
TODO
; +}; diff --git a/src/plugins/workspace/public/index.ts b/src/plugins/workspace/public/index.ts new file mode 100644 index 000000000000..817ad3ef0e1a --- /dev/null +++ b/src/plugins/workspace/public/index.ts @@ -0,0 +1,5 @@ +import { WorkspacesPlugin } from './plugin'; + +export function plugin() { + return new WorkspacesPlugin(); +} diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts new file mode 100644 index 000000000000..48253ee79a4b --- /dev/null +++ b/src/plugins/workspace/public/plugin.ts @@ -0,0 +1,38 @@ +import { i18n } from '@osd/i18n'; +import { + CoreSetup, + CoreStart, + Plugin, + AppMountParameters, + AppNavLinkStatus, +} from '../../../core/public'; +import { WORKSPACE_APP_ID } from '../common/constants'; + +export class WorkspacesPlugin implements Plugin<{}, {}> { + public setup(core: CoreSetup) { + core.application.register({ + id: WORKSPACE_APP_ID, + title: i18n.translate('workspace.settings.title', { + defaultMessage: 'Workspace', + }), + // order: 6010, + navLinkStatus: AppNavLinkStatus.hidden, + // updater$: this.appUpdater, + async mount(params: AppMountParameters) { + const { renderApp } = await import('./application'); + const [coreStart] = await core.getStartServices(); + const services = { + ...coreStart, + }; + + return renderApp(params, services); + }, + }); + + return {}; + } + + public start(core: CoreStart) { + return {}; + } +} From 62b34eb1818afd5bef386065246cdabd72c7436e Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Fri, 9 Jun 2023 15:58:25 +0800 Subject: [PATCH 02/54] test: add unit tests add license header Signed-off-by: Yulong Ruan --- src/plugins/workspace/common/constants.ts | 5 ++ src/plugins/workspace/public/application.tsx | 4 +- .../workspace/public/components/routes.ts | 7 ++- .../components/utils/breadcrumbs.test.ts | 50 +++++++++++++++++++ .../public/components/utils/breadcrumbs.ts | 39 +++++++++++++++ .../public/components/utils/path.test.ts | 18 +++++++ .../workspace/public/components/utils/path.ts | 17 +++++++ .../public/components/workspace_app.tsx | 35 ++++--------- .../components/workspace_creator/index.tsx | 5 ++ src/plugins/workspace/public/index.ts | 5 ++ src/plugins/workspace/public/plugin.ts | 5 ++ 11 files changed, 160 insertions(+), 30 deletions(-) create mode 100644 src/plugins/workspace/public/components/utils/breadcrumbs.test.ts create mode 100644 src/plugins/workspace/public/components/utils/breadcrumbs.ts create mode 100644 src/plugins/workspace/public/components/utils/path.test.ts create mode 100644 src/plugins/workspace/public/components/utils/path.ts diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 306fc5f3bef8..5ccad2c6a2a9 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -1,2 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index b005aa0e87ad..6715450ca693 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -11,11 +11,9 @@ import { AppMountParameters, CoreStart } from '../../../core/public'; import { OpenSearchDashboardsContextProvider } from '../../../plugins/opensearch_dashboards_react/public'; import { WorkspaceApp } from './components/workspace_app'; -interface Service extends CoreStart {} - export const renderApp = ( { element, history, appBasePath }: AppMountParameters, - services: Service + services: CoreStart ) => { ReactDOM.render( diff --git a/src/plugins/workspace/public/components/routes.ts b/src/plugins/workspace/public/components/routes.ts index 5155fa9a1146..5e47465643f5 100644 --- a/src/plugins/workspace/public/components/routes.ts +++ b/src/plugins/workspace/public/components/routes.ts @@ -1,10 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { WorkspaceCreator } from './workspace_creator'; export const paths = { create: '/create', }; -interface RouteConfig { +export interface RouteConfig { path: string; Component: React.ComponentType; label: string; diff --git a/src/plugins/workspace/public/components/utils/breadcrumbs.test.ts b/src/plugins/workspace/public/components/utils/breadcrumbs.test.ts new file mode 100644 index 000000000000..229fcde96055 --- /dev/null +++ b/src/plugins/workspace/public/components/utils/breadcrumbs.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createBreadcrumbsFromPath } from './breadcrumbs'; + +describe('breadcrumbs utils', () => { + const ROUTES = [ + { + path: '/create', + Component: jest.fn(), + label: 'Create', + }, + { + path: '/manage', + Component: jest.fn(), + label: 'Manage Workspaces', + }, + { + path: '/manage/access', + Component: jest.fn(), + label: 'Manage Access', + }, + ]; + + it('should create breadcrumbs with matched route', () => { + const breadcrumbs = createBreadcrumbsFromPath('/create', ROUTES, '/'); + expect(breadcrumbs).toEqual([{ href: '/', text: 'Workspace' }, { text: 'Create' }]); + }); + + it('should create breadcrumbs with only root route if path did not match any route', () => { + const breadcrumbs = createBreadcrumbsFromPath('/unknown', ROUTES, '/'); + expect(breadcrumbs).toEqual([{ href: '/', text: 'Workspace' }]); + }); + + it('should create breadcrumbs with all matched routes', () => { + const breadcrumbs = createBreadcrumbsFromPath('/manage/access', ROUTES, '/'); + expect(breadcrumbs).toEqual([ + { href: '/', text: 'Workspace' }, + { href: '/manage', text: 'Manage Workspaces' }, + { text: 'Manage Access' }, + ]); + }); + + it('should create breadcrumbs with only matched routes', () => { + const breadcrumbs = createBreadcrumbsFromPath('/manage/not-matched', ROUTES, '/'); + expect(breadcrumbs).toEqual([{ href: '/', text: 'Workspace' }, { text: 'Manage Workspaces' }]); + }); +}); diff --git a/src/plugins/workspace/public/components/utils/breadcrumbs.ts b/src/plugins/workspace/public/components/utils/breadcrumbs.ts new file mode 100644 index 000000000000..d6d302a9c6fc --- /dev/null +++ b/src/plugins/workspace/public/components/utils/breadcrumbs.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { matchPath } from 'react-router-dom'; + +import { RouteConfig } from '../routes'; +import { ChromeBreadcrumb } from '../../../../../core/public'; +import { WORKSPACE_APP_NAME } from '../../../common/constants'; +import { join } from './path'; + +export const createBreadcrumbsFromPath = ( + pathname: string, + routeConfig: RouteConfig[], + appBasePath: string +): ChromeBreadcrumb[] => { + const breadcrumbs: ChromeBreadcrumb[] = []; + while (pathname !== '/') { + const matchedRoute = routeConfig.find((route) => + matchPath(pathname, { path: route.path, exact: true }) + ); + if (matchedRoute) { + if (breadcrumbs.length === 0) { + breadcrumbs.unshift({ text: matchedRoute.label }); + } else { + breadcrumbs.unshift({ + text: matchedRoute.label, + href: join(appBasePath, matchedRoute.path), + }); + } + } + const pathArr = pathname.split('/'); + pathArr.pop(); + pathname = pathArr.join('/') ? pathArr.join('/') : '/'; + } + breadcrumbs.unshift({ text: WORKSPACE_APP_NAME, href: appBasePath }); + return breadcrumbs; +}; diff --git a/src/plugins/workspace/public/components/utils/path.test.ts b/src/plugins/workspace/public/components/utils/path.test.ts new file mode 100644 index 000000000000..d8bdf361d723 --- /dev/null +++ b/src/plugins/workspace/public/components/utils/path.test.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { join } from './path'; + +describe('path utils', () => { + it('should join paths', () => { + expect(join('/', '/')).toBe('/'); + expect(join('/', '/foo')).toBe('/foo'); + expect(join('foo', '/bar')).toBe('foo/bar'); + expect(join('foo', 'bar')).toBe('foo/bar'); + expect(join('foo', 'bar/baz')).toBe('foo/bar/baz'); + expect(join('/foo', 'bar/baz')).toBe('/foo/bar/baz'); + expect(join('/foo/', 'bar/baz')).toBe('/foo/bar/baz'); + }); +}); diff --git a/src/plugins/workspace/public/components/utils/path.ts b/src/plugins/workspace/public/components/utils/path.ts new file mode 100644 index 000000000000..1086a84b6d05 --- /dev/null +++ b/src/plugins/workspace/public/components/utils/path.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function join(base: string, ...paths: string[]) { + const normalized = [base] + .concat(...paths) + .join('/') + .split('/') + .filter(Boolean) + .join('/'); + if (base.startsWith('/')) { + return `/${normalized}`; + } + return normalized; +} diff --git a/src/plugins/workspace/public/components/workspace_app.tsx b/src/plugins/workspace/public/components/workspace_app.tsx index 9b3e7974039d..54c326bc551f 100644 --- a/src/plugins/workspace/public/components/workspace_app.tsx +++ b/src/plugins/workspace/public/components/workspace_app.tsx @@ -1,12 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import React, { useEffect } from 'react'; import { EuiPage, EuiPageBody } from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; -import { matchPath, Route, Switch, useLocation } from 'react-router-dom'; +import { Route, Switch, useLocation } from 'react-router-dom'; import { ROUTES } from './routes'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { ChromeBreadcrumb } from '../../../../core/public'; -import { WORKSPACE_APP_NAME } from '../../common/constants'; +import { createBreadcrumbsFromPath } from './utils/breadcrumbs'; export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { const { @@ -18,30 +22,9 @@ export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { * map the current pathname to breadcrumbs */ useEffect(() => { - let pathname = location.pathname; - const breadcrumbs: ChromeBreadcrumb[] = []; - - while (pathname !== '/') { - const matchedRoute = ROUTES.find((route) => - matchPath(pathname, { path: route.path, exact: true }) - ); - if (matchedRoute) { - if (breadcrumbs.length === 0) { - breadcrumbs.unshift({ text: matchedRoute.label }); - } else { - breadcrumbs.unshift({ - text: matchedRoute.label, - href: `${appBasePath}${matchedRoute.path}`, - }); - } - } - const pathArr = pathname.split('/'); - pathArr.pop(); - pathname = pathArr.join('/') ? pathArr.join('/') : '/'; - } - breadcrumbs.unshift({ text: WORKSPACE_APP_NAME, href: appBasePath }); + const breadcrumbs = createBreadcrumbsFromPath(location.pathname, ROUTES, appBasePath); chrome?.setBreadcrumbs(breadcrumbs); - }, [appBasePath, location.pathname, chrome?.setBreadcrumbs]); + }, [appBasePath, location.pathname, chrome]); return ( diff --git a/src/plugins/workspace/public/components/workspace_creator/index.tsx b/src/plugins/workspace/public/components/workspace_creator/index.tsx index 0a1601db8966..11a3e0feaf92 100644 --- a/src/plugins/workspace/public/components/workspace_creator/index.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/index.tsx @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import React from 'react'; export const WorkspaceCreator = () => { diff --git a/src/plugins/workspace/public/index.ts b/src/plugins/workspace/public/index.ts index 817ad3ef0e1a..9f5c720fc9d5 100644 --- a/src/plugins/workspace/public/index.ts +++ b/src/plugins/workspace/public/index.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { WorkspacesPlugin } from './plugin'; export function plugin() { diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 48253ee79a4b..13017dab8835 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + import { i18n } from '@osd/i18n'; import { CoreSetup, From 146e6c94213c1b4ddfc145694000f7e7bdfc7a93 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 9 Jun 2023 11:13:00 +0800 Subject: [PATCH 03/54] workspace template init commit Signed-off-by: Hailong Cui --- src/core/public/application/types.ts | 8 +++- src/core/public/index.ts | 1 + src/core/types/index.ts | 1 + src/core/types/workspace_template.ts | 31 +++++++++++++++ src/core/utils/default_workspace_templates.ts | 38 +++++++++++++++++++ src/core/utils/index.ts | 1 + src/plugins/dashboard/public/plugin.tsx | 2 + 7 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/core/types/workspace_template.ts create mode 100644 src/core/utils/default_workspace_templates.ts diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 7398aad65009..66606775a772 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -45,7 +45,7 @@ import { OverlayStart } from '../overlays'; import { PluginOpaqueId } from '../plugins'; import { IUiSettingsClient } from '../ui_settings'; import { SavedObjectsStart } from '../saved_objects'; -import { AppCategory } from '../../types'; +import { AppCategory, WorkspaceTemplate } from '../../types'; import { ScopedHistory } from './scoped_history'; /** @@ -123,6 +123,12 @@ export interface App { */ category?: AppCategory; + /** + * The template definition of features belongs to + * See {@link WorkspaceTemplate} + */ + workspaceTemplate?: WorkspaceTemplate[]; + /** * The initial status of the application. * Defaulting to `accessible` diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9a38771f513e..8698c4bc727c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -93,6 +93,7 @@ export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ export { CoreContext, CoreSystem } from './core_system'; export { DEFAULT_APP_CATEGORIES } from '../utils'; +export { DEFAULT_WORKSPACE_TEMPLATES } from '../utils'; export { AppCategory, UiSettingsParams, diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 9f620273e3b2..e016d7ca7527 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -35,6 +35,7 @@ export * from './core_service'; export * from './capabilities'; export * from './app_category'; +export * from './workspace_template'; export * from './ui_settings'; export * from './saved_objects'; export * from './serializable'; diff --git a/src/core/types/workspace_template.ts b/src/core/types/workspace_template.ts new file mode 100644 index 000000000000..885c826120c5 --- /dev/null +++ b/src/core/types/workspace_template.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface WorkspaceTemplate { + /** + * Unique identifier for the workspace template + */ + id: string; + + /** + * Label used for workspace template name. + */ + label: string; + + /** + * The order that workspace template will be sorted in + */ + order?: number; + + /** + * Introduction of the template + */ + description: string; + + /** + * Sample screenshot image location + */ + screenshot?: string; +} diff --git a/src/core/utils/default_workspace_templates.ts b/src/core/utils/default_workspace_templates.ts new file mode 100644 index 000000000000..153fc23f790a --- /dev/null +++ b/src/core/utils/default_workspace_templates.ts @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WorkspaceTemplate } from '../types'; + +/** @internal */ +export const DEFAULT_WORKSPACE_TEMPLATES: Record = Object.freeze({ + search: { + id: 'search', + label: 'Search', + order: 1000, + description: + "Intro paragraph blur about search, key features, and why you'd want to create ana search workspace", + }, + observability: { + id: 'observability', + label: 'Observability', + order: 2000, + description: + "Intro paragraph blur about observability, key features, and why you'd want to create ana observability workspace", + }, + security_analytics: { + id: 'security_analytics', + label: 'Security Analytics', + order: 3000, + description: + "Intro paragraph blur about security analytics, key features, and why you'd want to create ana security analytics workspace", + }, + general_analysis: { + id: 'general_analysis', + label: 'General Analytics', + order: 4000, + description: + "Intro paragraph blur about analytics, key features, and why you'd want to create ana analytics workspace", + }, +}); diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index b3b6ce4aab02..797d90c18130 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,3 +37,4 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; +export { DEFAULT_WORKSPACE_TEMPLATES } from './default_workspace_templates'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 87f934925899..95c6e5d4bafd 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -80,6 +80,7 @@ import { } from '../../opensearch_dashboards_legacy/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../../plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { DEFAULT_WORKSPACE_TEMPLATES } from '../../../core/public'; import { ACTION_CLONE_PANEL, @@ -366,6 +367,7 @@ export class DashboardPlugin defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + workspaceTemplate: [DEFAULT_WORKSPACE_TEMPLATES.search], mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart, dashboardStart] = await core.getStartServices(); this.currentHistory = params.history; From b84a894485b614dfd7123185646127d447c31b98 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 9 Jun 2023 12:07:30 +0800 Subject: [PATCH 04/54] refacter workspace template into hooks Signed-off-by: Hailong Cui --- src/plugins/workspace/public/hooks.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/plugins/workspace/public/hooks.ts diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts new file mode 100644 index 000000000000..a0b13ec911be --- /dev/null +++ b/src/plugins/workspace/public/hooks.ts @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApplicationStart, PublicAppInfo } from 'opensearch-dashboards/public'; +import { WorkspaceTemplate } from '../../../core/types'; + +export function useWorkspaceTemplate(application: ApplicationStart) { + let workspaceTemplates = [] as WorkspaceTemplate[]; + const templateFeatureMap = new Map(); + + application.applications$.subscribe((applications) => + applications.forEach((app) => { + const { workspaceTemplate: templates = [] } = app; + workspaceTemplates.push(...templates); + for (const template of templates) { + const features = templateFeatureMap.get(template.id) || []; + features.push(app); + templateFeatureMap.set(template.id, features); + } + }) + ); + + workspaceTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); + workspaceTemplates = [...new Set(workspaceTemplates)]; + + return { + workspaceTemplates, + templateFeatureMap, + }; +} From 619283149fe23c7edf45c62608057273057a64aa Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 9 Jun 2023 17:33:48 +0800 Subject: [PATCH 05/54] refacter workspace template hooks Signed-off-by: Hailong Cui --- src/core/types/workspace_template.ts | 2 +- src/plugins/workspace/public/hooks.ts | 41 ++++++++++++++------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/core/types/workspace_template.ts b/src/core/types/workspace_template.ts index 885c826120c5..8d8f91ca3ed8 100644 --- a/src/core/types/workspace_template.ts +++ b/src/core/types/workspace_template.ts @@ -27,5 +27,5 @@ export interface WorkspaceTemplate { /** * Sample screenshot image location */ - screenshot?: string; + coverImage?: string; } diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index a0b13ec911be..71019c5948a2 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -4,29 +4,32 @@ */ import { ApplicationStart, PublicAppInfo } from 'opensearch-dashboards/public'; +import { useObservable } from 'react-use'; +import { useMemo } from 'react'; import { WorkspaceTemplate } from '../../../core/types'; export function useWorkspaceTemplate(application: ApplicationStart) { - let workspaceTemplates = [] as WorkspaceTemplate[]; - const templateFeatureMap = new Map(); + const applications = useObservable(application.applications$); - application.applications$.subscribe((applications) => - applications.forEach((app) => { - const { workspaceTemplate: templates = [] } = app; - workspaceTemplates.push(...templates); - for (const template of templates) { - const features = templateFeatureMap.get(template.id) || []; - features.push(app); - templateFeatureMap.set(template.id, features); - } - }) - ); + return useMemo(() => { + let workspaceTemplates = [] as WorkspaceTemplate[]; + const templateFeatureMap = new Map(); - workspaceTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); - workspaceTemplates = [...new Set(workspaceTemplates)]; + if (applications) { + applications.forEach((app) => { + const { workspaceTemplate: templates = [] } = app; + workspaceTemplates.push(...templates); + for (const template of templates) { + const features = templateFeatureMap.get(template.id) || []; + features.push(app); + templateFeatureMap.set(template.id, features); + } + }); - return { - workspaceTemplates, - templateFeatureMap, - }; + workspaceTemplates = [...new Set(workspaceTemplates)]; + workspaceTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); + } + + return { workspaceTemplates, templateFeatureMap }; + }, [applications]); } From 65363da5bf79c0d55a51260d71cb2c8dc326e98f Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 9 Jun 2023 17:40:05 +0800 Subject: [PATCH 06/54] update coverImage comments Signed-off-by: Hailong Cui --- src/core/types/workspace_template.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/types/workspace_template.ts b/src/core/types/workspace_template.ts index 8d8f91ca3ed8..4be8c4882bf0 100644 --- a/src/core/types/workspace_template.ts +++ b/src/core/types/workspace_template.ts @@ -25,7 +25,7 @@ export interface WorkspaceTemplate { description: string; /** - * Sample screenshot image location + * template coverage image location */ coverImage?: string; } From d661cf37aa9bea31328bbc66a1934bbceb68edb9 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Wed, 7 Jun 2023 15:32:24 +0800 Subject: [PATCH 07/54] feature: add public/workspaces service Signed-off-by: SuZhoue-Joe --- src/core/public/application/types.ts | 3 + src/core/public/core_system.ts | 6 + src/core/public/index.ts | 3 + src/core/public/plugins/plugin_context.ts | 1 + src/core/public/workspace/index.ts | 7 + .../public/workspace/workspaces_client.ts | 167 ++++++++++++++++++ .../public/workspace/workspaces_service.ts | 22 +++ 7 files changed, 209 insertions(+) create mode 100644 src/core/public/workspace/index.ts create mode 100644 src/core/public/workspace/workspaces_client.ts create mode 100644 src/core/public/workspace/workspaces_service.ts diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 7398aad65009..4744ab34cfd3 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -47,6 +47,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; +import { WorkspacesStart } from '../workspace'; /** * Accessibility status of an application. @@ -334,6 +335,8 @@ export interface AppMountContext { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; + /** {@link WorkspacesService} */ + workspaces: WorkspacesStart; }; } diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 8fe5a36ebb55..27f39ea57c1b 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -54,6 +54,7 @@ import { ContextService } from './context'; import { IntegrationsService } from './integrations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; +import { WorkspacesService } from './workspace'; interface Params { rootDomElement: HTMLElement; @@ -110,6 +111,7 @@ export class CoreSystem { private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; + private readonly workspaces: WorkspacesService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -138,6 +140,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); + this.workspaces = new WorkspacesService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; @@ -199,6 +202,7 @@ export class CoreSystem { const uiSettings = await this.uiSettings.start(); const docLinks = this.docLinks.start({ injectedMetadata }); const http = await this.http.start(); + const workspaces = await this.workspaces.start({ http }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const fatalErrors = await this.fatalErrors.start(); @@ -242,6 +246,7 @@ export class CoreSystem { overlays, savedObjects, uiSettings, + workspaces, })); const core: InternalCoreStart = { @@ -256,6 +261,7 @@ export class CoreSystem { overlays, uiSettings, fatalErrors, + workspaces, }; await this.plugins.start(core); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9a38771f513e..fc116c2d2d47 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -88,6 +88,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; +import { WorkspacesStart } from './workspace'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ @@ -293,6 +294,8 @@ export interface CoreStart { getInjectedVar: (name: string, defaultValue?: any) => unknown; getBranding: () => Branding; }; + /** {@link WorkspacesStart} */ + workspaces: WorkspacesStart; } export { diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 42c40e91183f..81fcb2b34dab 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -168,5 +168,6 @@ export function createPluginStartContext< getBranding: deps.injectedMetadata.getBranding, }, fatalErrors: deps.fatalErrors, + workspaces: deps.workspaces, }; } diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts new file mode 100644 index 000000000000..a70d91733ab6 --- /dev/null +++ b/src/core/public/workspace/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { WorkspacesClientContract, WorkspacesClient } from './workspaces_client'; +export { WorkspacesStart, WorkspacesService } from './workspaces_service'; +export { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts new file mode 100644 index 000000000000..893546ef1f0b --- /dev/null +++ b/src/core/public/workspace/workspaces_client.ts @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { resolve as resolveUrl } from 'url'; +import type { PublicMethodsOf } from '@osd/utility-types'; +import { HttpStart } from '../http'; +import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; + +/** + * WorkspacesClientContract as implemented by the {@link WorkspacesClient} + * + * @public + */ +export type WorkspacesClientContract = PublicMethodsOf; + +const API_BASE_URL = '/api/workspaces/'; + +const join = (...uriComponents: Array) => + uriComponents + .filter((comp): comp is string => Boolean(comp)) + .map(encodeURIComponent) + .join('/'); + +/** + * Workspaces is OpenSearchDashboards's visualize mechanism allowing admins to + * organize related features + * + * @public + */ +export class WorkspacesClient { + private http: HttpStart; + /** @internal */ + constructor(http: HttpStart) { + this.http = http; + } + + private async performBulkGet(objects: Array<{ id: string }>): Promise { + const path = this.getPath(['_bulk_get']); + return this.http.fetch(path, { + method: 'POST', + body: JSON.stringify(objects), + }); + } + + private getPath(path: Array): string { + return resolveUrl(API_BASE_URL, join(...path)); + } + + /** + * Persists an workspace + * + * @param attributes + * @returns + */ + public create = (attributes: Omit): Promise => { + if (!attributes) { + return Promise.reject(new Error('requires attributes')); + } + + const path = this.getPath([]); + + return this.http.fetch(path, { + method: 'POST', + body: JSON.stringify({ + attributes, + }), + }); + }; + + /** + * Deletes a workspace + * + * @param id + * @returns + */ + public delete = (id: string): Promise<{ success: boolean }> => { + if (!id) { + return Promise.reject(new Error('requires id')); + } + + return this.http.delete(this.getPath([id]), { method: 'DELETE' }); + }; + + /** + * Search for workspaces + * + * @param {object} [options={}] + * @property {string} options.search + * @property {string} options.search_fields - see OpenSearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.per_page=20] + * @property {array} options.fields + * @returns A find result with workspaces matching the specified search. + */ + public list = ( + options?: WorkspaceFindOptions + ): Promise< + WorkspaceAttribute & { + total: number; + perPage: number; + page: number; + } + > => { + const path = this.getPath(['_list']); + return this.http.fetch(path, { + method: 'GET', + query: options, + }); + }; + + /** + * Fetches a single workspace + * + * @param {string} id + * @returns The workspace for the given id. + */ + public get = (id: string): Promise => { + if (!id) { + return Promise.reject(new Error('requires id')); + } + + return this.performBulkGet([{ id }]).then((res) => { + if (res.length) { + return res[0]; + } + + return Promise.reject('No workspace can be found'); + }); + }; + + /** + * Updates a workspace + * + * @param {string} type + * @param {string} id + * @param {object} attributes + * @returns + */ + public update( + id: string, + attributes: Partial + ): Promise<{ + success: boolean; + }> { + if (!id || !attributes) { + return Promise.reject(new Error('requires id and attributes')); + } + + const path = this.getPath([id]); + const body = { + attributes, + }; + + return this.http + .fetch(path, { + method: 'PUT', + body: JSON.stringify(body), + }) + .then((resp: WorkspaceAttribute) => { + return { + success: true, + }; + }); + } +} diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts new file mode 100644 index 000000000000..f691ebbe3e16 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CoreService } from 'src/core/types'; +import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; +import { HttpStart } from '..'; + +/** + * @public + */ +export interface WorkspacesStart { + client: WorkspacesClientContract; +} + +export class WorkspacesService implements CoreService { + public async setup() {} + public async start({ http }: { http: HttpStart }): Promise { + return { client: new WorkspacesClient(http) }; + } + public async stop() {} +} From 580343ad9368b384570b57db8f7a8dd2753a5a4e Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 8 Jun 2023 16:34:15 +0800 Subject: [PATCH 08/54] feat: add interfaces for workspaces client Signed-off-by: SuZhoue-Joe --- .../public/workspace/workspaces_client.ts | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 893546ef1f0b..1ec4e657eba9 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -4,6 +4,7 @@ */ import { resolve as resolveUrl } from 'url'; import type { PublicMethodsOf } from '@osd/utility-types'; +import { WORKSPACES_API_BASE_URL } from '../../server/types'; import { HttpStart } from '../http'; import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; @@ -14,14 +15,18 @@ import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; */ export type WorkspacesClientContract = PublicMethodsOf; -const API_BASE_URL = '/api/workspaces/'; - const join = (...uriComponents: Array) => uriComponents .filter((comp): comp is string => Boolean(comp)) .map(encodeURIComponent) .join('/'); +interface IResponse { + result: T; + success: boolean; + error?: string; +} + /** * Workspaces is OpenSearchDashboards's visualize mechanism allowing admins to * organize related features @@ -35,16 +40,32 @@ export class WorkspacesClient { this.http = http; } - private async performBulkGet(objects: Array<{ id: string }>): Promise { - const path = this.getPath(['_bulk_get']); - return this.http.fetch(path, { - method: 'POST', - body: JSON.stringify(objects), - }); + private getPath(path: Array): string { + return resolveUrl(`${WORKSPACES_API_BASE_URL}/`, join(...path)); } - private getPath(path: Array): string { - return resolveUrl(API_BASE_URL, join(...path)); + public async enterWorkspace(id: string): Promise> { + return { + result: false, + success: false, + error: 'Unimplement', + }; + } + + public async exitWorkspace(): Promise> { + return { + result: false, + success: false, + error: 'Unimplement', + }; + } + + public async getCurrentWorkspace(): Promise> { + return { + result: false, + success: false, + error: 'Unimplement', + }; } /** @@ -121,12 +142,9 @@ export class WorkspacesClient { return Promise.reject(new Error('requires id')); } - return this.performBulkGet([{ id }]).then((res) => { - if (res.length) { - return res[0]; - } - - return Promise.reject('No workspace can be found'); + const path = this.getPath([id]); + return this.http.fetch(path, { + method: 'GET', }); }; From a276998e7559e6b1795c34712b357cc789555103 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 8 Jun 2023 16:48:24 +0800 Subject: [PATCH 09/54] feat: add interfaces for workspaces client Signed-off-by: SuZhoue-Joe --- .../public/workspace/workspaces_client.ts | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 1ec4e657eba9..076f14d4696e 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -21,11 +21,15 @@ const join = (...uriComponents: Array) => .map(encodeURIComponent) .join('/'); -interface IResponse { - result: T; - success: boolean; - error?: string; -} +type IResponse = + | { + result: T; + success: true; + } + | { + success: false; + error?: string; + }; /** * Workspaces is OpenSearchDashboards's visualize mechanism allowing admins to @@ -35,7 +39,6 @@ interface IResponse { */ export class WorkspacesClient { private http: HttpStart; - /** @internal */ constructor(http: HttpStart) { this.http = http; } @@ -44,25 +47,22 @@ export class WorkspacesClient { return resolveUrl(`${WORKSPACES_API_BASE_URL}/`, join(...path)); } - public async enterWorkspace(id: string): Promise> { + public async enterWorkspace(id: string): Promise> { return { - result: false, success: false, error: 'Unimplement', }; } - public async exitWorkspace(): Promise> { + public async exitWorkspace(): Promise> { return { - result: false, success: false, error: 'Unimplement', }; } - public async getCurrentWorkspace(): Promise> { + public async getCurrentWorkspace(): Promise> { return { - result: false, success: false, error: 'Unimplement', }; @@ -74,7 +74,9 @@ export class WorkspacesClient { * @param attributes * @returns */ - public create = (attributes: Omit): Promise => { + public create = ( + attributes: Omit + ): Promise> => { if (!attributes) { return Promise.reject(new Error('requires attributes')); } @@ -95,7 +97,7 @@ export class WorkspacesClient { * @param id * @returns */ - public delete = (id: string): Promise<{ success: boolean }> => { + public delete = (id: string): Promise> => { if (!id) { return Promise.reject(new Error('requires id')); } @@ -118,11 +120,13 @@ export class WorkspacesClient { public list = ( options?: WorkspaceFindOptions ): Promise< - WorkspaceAttribute & { - total: number; - perPage: number; - page: number; - } + IResponse< + WorkspaceAttribute & { + total: number; + perPage: number; + page: number; + } + > > => { const path = this.getPath(['_list']); return this.http.fetch(path, { @@ -137,7 +141,7 @@ export class WorkspacesClient { * @param {string} id * @returns The workspace for the given id. */ - public get = (id: string): Promise => { + public get = (id: string): Promise> => { if (!id) { return Promise.reject(new Error('requires id')); } @@ -151,17 +155,11 @@ export class WorkspacesClient { /** * Updates a workspace * - * @param {string} type * @param {string} id * @param {object} attributes * @returns */ - public update( - id: string, - attributes: Partial - ): Promise<{ - success: boolean; - }> { + public update(id: string, attributes: Partial): Promise> { if (!id || !attributes) { return Promise.reject(new Error('requires id and attributes')); } @@ -178,6 +176,7 @@ export class WorkspacesClient { }) .then((resp: WorkspaceAttribute) => { return { + result: true, success: true, }; }); From 02f73fa9bc3197edfb3049883ad7d6ee34e69ea1 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Thu, 8 Jun 2023 16:54:34 +0800 Subject: [PATCH 10/54] feat: add interfaces for workspaces client Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/workspaces_client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 076f14d4696e..110a61ca1189 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -61,6 +61,13 @@ export class WorkspacesClient { }; } + public async getCurrentWorkspaceId(): Promise> { + return { + success: false, + error: 'Unimplement', + }; + } + public async getCurrentWorkspace(): Promise> { return { success: false, From f5d01c3180ee8bc780fb3e6526bdc27440904421 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 12 Jun 2023 12:00:48 +0800 Subject: [PATCH 11/54] feat: implement workspaces service Signed-off-by: SuZhoue-Joe --- src/core/server/internal_types.ts | 3 + src/core/server/server.ts | 14 ++ src/core/server/types.ts | 1 + src/core/server/workspaces/index.ts | 13 ++ src/core/server/workspaces/routes/index.ts | 146 ++++++++++++++++ .../server/workspaces/saved_objects/index.ts | 6 + .../workspaces/saved_objects/workspace.ts | 46 +++++ src/core/server/workspaces/types.ts | 71 ++++++++ .../workspaces_client_with_saved_object.ts | 157 ++++++++++++++++++ .../server/workspaces/workspaces_service.ts | 74 +++++++++ 10 files changed, 531 insertions(+) create mode 100644 src/core/server/workspaces/index.ts create mode 100644 src/core/server/workspaces/routes/index.ts create mode 100644 src/core/server/workspaces/saved_objects/index.ts create mode 100644 src/core/server/workspaces/saved_objects/workspace.ts create mode 100644 src/core/server/workspaces/types.ts create mode 100644 src/core/server/workspaces/workspaces_client_with_saved_object.ts create mode 100644 src/core/server/workspaces/workspaces_service.ts diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 2b4df7da68bf..0eb0b684d4f4 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -48,6 +48,7 @@ import { InternalStatusServiceSetup } from './status'; import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; +import { InternalWorkspacesServiceSetup, InternalWorkspacesServiceStart } from './workspaces'; /** @internal */ export interface InternalCoreSetup { @@ -64,6 +65,7 @@ export interface InternalCoreSetup { auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; + workspaces: InternalWorkspacesServiceSetup; } /** @@ -78,6 +80,7 @@ export interface InternalCoreStart { uiSettings: InternalUiSettingsServiceStart; auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; + workspaces: InternalWorkspacesServiceStart; } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index d4c041725ac7..99c79351d861 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -62,6 +62,7 @@ import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; import { CoreRouteHandlerContext } from './core_route_handler_context'; +import { WorkspacesService } from './workspaces'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -86,6 +87,7 @@ export class Server { private readonly coreApp: CoreApp; private readonly auditTrail: AuditTrailService; private readonly coreUsageData: CoreUsageDataService; + private readonly workspaces: WorkspacesService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -118,6 +120,7 @@ export class Server { this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); + this.workspaces = new WorkspacesService(core); } public async setup() { @@ -172,6 +175,11 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const workspacesSetup = await this.workspaces.setup({ + http: httpSetup, + savedObject: savedObjectsSetup, + }); + const statusSetup = await this.status.setup({ opensearch: opensearchServiceSetup, pluginDependencies: pluginTree.asNames, @@ -212,6 +220,7 @@ export class Server { auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, + workspaces: workspacesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -253,6 +262,9 @@ export class Server { opensearch: opensearchStart, savedObjects: savedObjectsStart, }); + const workspacesStart = await this.workspaces.start({ + savedObjects: savedObjectsStart, + }); this.coreStart = { capabilities: capabilitiesStart, @@ -263,6 +275,7 @@ export class Server { uiSettings: uiSettingsStart, auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, + workspaces: workspacesStart, }; const pluginsStart = await this.plugins.start(this.coreStart); @@ -295,6 +308,7 @@ export class Server { await this.status.stop(); await this.logging.stop(); await this.auditTrail.stop(); + await this.workspaces.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 90ccef575807..f6e54c201dae 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -35,3 +35,4 @@ export * from './ui_settings/types'; export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@osd/config'; export { Branding } from '../../core/types'; +export * from './workspaces/types'; diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts new file mode 100644 index 000000000000..838f216bbd86 --- /dev/null +++ b/src/core/server/workspaces/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { + WorkspacesService, + InternalWorkspacesServiceSetup, + WorkspacesServiceStart, + WorkspacesServiceSetup, + InternalWorkspacesServiceStart, +} from './workspaces_service'; + +export { WorkspaceAttribute, WorkspaceFindOptions, WORKSPACES_API_BASE_URL } from './types'; diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts new file mode 100644 index 000000000000..dbd7b20809e2 --- /dev/null +++ b/src/core/server/workspaces/routes/index.ts @@ -0,0 +1,146 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { schema } from '@osd/config-schema'; +import { InternalHttpServiceSetup } from '../../http'; +import { Logger } from '../../logging'; +import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL } from '../types'; + +export function registerRoutes({ + client, + logger, + http, +}: { + client: IWorkspaceDBImpl; + logger: Logger; + http: InternalHttpServiceSetup; +}) { + const router = http.createRouter(WORKSPACES_API_BASE_URL); + router.get( + { + path: '/_list', + validate: { + query: schema.object({ + per_page: schema.number({ min: 0, defaultValue: 20 }), + page: schema.number({ min: 0, defaultValue: 1 }), + sort_field: schema.maybe(schema.string()), + fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const result = await client.list( + { + context, + request: req, + logger, + }, + req.query + ); + return res.ok({ body: result }); + }) + ); + router.get( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + const result = await client.get( + { + context, + request: req, + logger, + }, + id + ); + return res.ok({ body: result }); + }) + ); + router.post( + { + path: '/{id?}', + validate: { + body: schema.object({ + attributes: schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + }), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { attributes } = req.body; + + const result = await client.create( + { + context, + request: req, + logger, + }, + attributes + ); + return res.ok({ body: result }); + }) + ); + router.put( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + attributes: schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + }), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + const { attributes } = req.body; + + const result = await client.update( + { + context, + request: req, + logger, + }, + id, + attributes + ); + return res.ok({ body: result }); + }) + ); + router.delete( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + + const result = await client.delete( + { + context, + request: req, + logger, + }, + id + ); + return res.ok({ body: result }); + }) + ); +} diff --git a/src/core/server/workspaces/saved_objects/index.ts b/src/core/server/workspaces/saved_objects/index.ts new file mode 100644 index 000000000000..51653c50681e --- /dev/null +++ b/src/core/server/workspaces/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { workspace } from './workspace'; diff --git a/src/core/server/workspaces/saved_objects/workspace.ts b/src/core/server/workspaces/saved_objects/workspace.ts new file mode 100644 index 000000000000..d211aaa3ea93 --- /dev/null +++ b/src/core/server/workspaces/saved_objects/workspace.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'opensearch-dashboards/server'; + +export const workspace: SavedObjectsType = { + name: 'workspace', + namespaceType: 'agnostic', + hidden: false, + management: { + icon: 'apps', // todo: pending ux #2034 + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/opensearch-dashboards/dataSources/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/management/opensearch-dashboards/dataSources/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'management.opensearchDashboards.dataSources', + }; + }, + }, + mappings: { + dynamic: false, + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + /** + * In opensearch, string[] is also mapped to text + */ + features: { + type: 'text', + }, + }, + }, +}; diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts new file mode 100644 index 000000000000..cc24797605e2 --- /dev/null +++ b/src/core/server/workspaces/types.ts @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + Logger, + OpenSearchDashboardsRequest, + RequestHandlerContext, + SavedObjectsFindResponse, +} from '..'; +import { WorkspacesSetupDeps } from './workspaces_service'; + +export interface WorkspaceAttribute { + id: string; + name: string; + description?: string; + features?: string[]; +} + +export interface WorkspaceFindOptions { + page?: number; + per_page?: number; + search?: string; + search_fields?: string[]; + sort_field?: string; + sort_order?: string; +} + +export interface IRequestDetail { + request: OpenSearchDashboardsRequest; + context: RequestHandlerContext; + logger: Logger; +} + +export interface IWorkspaceDBImpl { + setup(dep: WorkspacesSetupDeps): Promise>; + create( + requestDetail: IRequestDetail, + payload: Omit + ): Promise>; + list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): Promise< + IResponse< + { + workspaces: WorkspaceAttribute[]; + } & Pick + > + >; + get(requestDetail: IRequestDetail, id: string): Promise>; + update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise>; + delete(requestDetail: IRequestDetail, id: string): Promise>; + destroy(): Promise>; +} + +export type IResponse = + | { + result: T; + success: true; + } + | { + success: false; + error?: string; + }; + +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; diff --git a/src/core/server/workspaces/workspaces_client_with_saved_object.ts b/src/core/server/workspaces/workspaces_client_with_saved_object.ts new file mode 100644 index 000000000000..058d7477efd4 --- /dev/null +++ b/src/core/server/workspaces/workspaces_client_with_saved_object.ts @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { SavedObject, SavedObjectError, SavedObjectsClientContract } from '../types'; +import { + IWorkspaceDBImpl, + WorkspaceAttribute, + WorkspaceFindOptions, + IResponse, + IRequestDetail, +} from './types'; +import { WorkspacesSetupDeps } from './workspaces_service'; +import { workspace } from './saved_objects'; + +export const WORKSPACES_TYPE_FOR_SAVED_OBJECT = 'workspace'; + +export class WorkspacesClientWithSavedObject implements IWorkspaceDBImpl { + private setupDep: WorkspacesSetupDeps; + constructor(dep: WorkspacesSetupDeps) { + this.setupDep = dep; + } + private getSavedObjectClientsFromRequestDetail( + requestDetail: IRequestDetail + ): SavedObjectsClientContract { + return requestDetail.context.core.savedObjects.client; + } + private getFlatternedResultWithSavedObject( + savedObject: SavedObject + ): WorkspaceAttribute { + return { + ...savedObject.attributes, + id: savedObject.id, + }; + } + private formatError(error: SavedObjectError | Error | any): string { + return error.message || error.error || 'Error'; + } + public async setup(dep: WorkspacesSetupDeps): Promise> { + this.setupDep.savedObject.registerType(workspace); + return { + success: true, + result: true, + }; + } + public async create( + requestDetail: IRequestDetail, + payload: Omit + ): ReturnType { + try { + const result = await this.getSavedObjectClientsFromRequestDetail(requestDetail).create< + Omit + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, payload); + return { + success: true, + result: { + id: result.id, + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): ReturnType { + try { + const { + saved_objects: savedObjects, + ...others + } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( + { + ...options, + type: WORKSPACES_TYPE_FOR_SAVED_OBJECT, + } + ); + return { + success: true, + result: { + ...others, + workspaces: savedObjects.map((item) => this.getFlatternedResultWithSavedObject(item)), + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async get( + requestDetail: IRequestDetail, + id: string + ): Promise> { + try { + const result = await this.getSavedObjectClientsFromRequestDetail(requestDetail).get< + WorkspaceAttribute + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, id); + return { + success: true, + result: this.getFlatternedResultWithSavedObject(result), + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise> { + try { + await this.getSavedObjectClientsFromRequestDetail(requestDetail).update< + Omit + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, id, payload); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async delete(requestDetail: IRequestDetail, id: string): Promise> { + try { + await this.getSavedObjectClientsFromRequestDetail(requestDetail).delete( + WORKSPACES_TYPE_FOR_SAVED_OBJECT, + id + ); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async destroy(): Promise> { + return { + success: true, + result: true, + }; + } +} diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts new file mode 100644 index 000000000000..15c150b7378a --- /dev/null +++ b/src/core/server/workspaces/workspaces_service.ts @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { InternalHttpServiceSetup } from '../http'; +import { Logger } from '../logging'; +import { registerRoutes } from './routes'; +import { + InternalSavedObjectsServiceSetup, + InternalSavedObjectsServiceStart, +} from '../saved_objects'; +import { IWorkspaceDBImpl } from './types'; +import { WorkspacesClientWithSavedObject } from './workspaces_client_with_saved_object'; + +export interface WorkspacesServiceSetup { + setWorkspacesClient: (client: IWorkspaceDBImpl) => void; +} + +export interface WorkspacesServiceStart { + client: IWorkspaceDBImpl; +} + +export interface WorkspacesSetupDeps { + http: InternalHttpServiceSetup; + savedObject: InternalSavedObjectsServiceSetup; +} + +export type InternalWorkspacesServiceSetup = WorkspacesServiceSetup; +export type InternalWorkspacesServiceStart = WorkspacesServiceStart; + +/** @internal */ +export interface WorkspacesStartDeps { + savedObjects: InternalSavedObjectsServiceStart; +} + +export class WorkspacesService + implements CoreService { + private logger: Logger; + private client?: IWorkspaceDBImpl; + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('workspaces-service'); + } + + public async setup(setupDeps: WorkspacesSetupDeps): Promise { + this.logger.debug('Setting up Workspaces service'); + + this.client = this.client || new WorkspacesClientWithSavedObject(setupDeps); + await this.client.setup(setupDeps); + + registerRoutes({ + http: setupDeps.http, + logger: this.logger, + client: this.client as IWorkspaceDBImpl, + }); + + return { + setWorkspacesClient: (client: IWorkspaceDBImpl) => { + this.client = client; + }, + }; + } + + public async start(deps: WorkspacesStartDeps): Promise { + this.logger.debug('Starting SavedObjects service'); + + return { + client: this.client as IWorkspaceDBImpl, + }; + } + + public async stop() {} +} From e302b05b736cad025e9a3987bc95f1e6df9ba807 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Tue, 13 Jun 2023 08:27:20 +0800 Subject: [PATCH 12/54] feat: changes to client type interface Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/workspaces_client.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 110a61ca1189..fefdd261d544 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -127,13 +127,12 @@ export class WorkspacesClient { public list = ( options?: WorkspaceFindOptions ): Promise< - IResponse< - WorkspaceAttribute & { - total: number; - perPage: number; - page: number; - } - > + IResponse<{ + workspaces: WorkspaceAttribute[]; + total: number; + per_page: number; + page: number; + }> > => { const path = this.getPath(['_list']); return this.http.fetch(path, { From 790de17e172169ddaec056c7ccd5f436d0963252 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Tue, 13 Jun 2023 08:47:25 +0800 Subject: [PATCH 13/54] feat: changes to client implement Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/workspaces_client.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index fefdd261d544..fbb8047c9b3b 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -175,16 +175,9 @@ export class WorkspacesClient { attributes, }; - return this.http - .fetch(path, { - method: 'PUT', - body: JSON.stringify(body), - }) - .then((resp: WorkspaceAttribute) => { - return { - result: true, - success: true, - }; - }); + return this.http.fetch(path, { + method: 'PUT', + body: JSON.stringify(body), + }); } } From 562918976bd9c76415b44320fa92f2b93af5eeea Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Tue, 13 Jun 2023 13:43:59 +0800 Subject: [PATCH 14/54] feat: implement more for workspaces service Signed-off-by: SuZhoue-Joe --- .../public/workspace/workspaces_client.ts | 39 ++++++---- src/core/server/workspaces/routes/index.ts | 76 ++++++++++++++++++- src/core/server/workspaces/types.ts | 2 + 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index fbb8047c9b3b..e275166f9139 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -48,31 +48,38 @@ export class WorkspacesClient { } public async enterWorkspace(id: string): Promise> { - return { - success: false, - error: 'Unimplement', - }; + return this.http.post(this.getPath(['_enter', id])); } public async exitWorkspace(): Promise> { - return { - success: false, - error: 'Unimplement', - }; + return this.http.post(this.getPath(['_exit'])); } public async getCurrentWorkspaceId(): Promise> { - return { - success: false, - error: 'Unimplement', - }; + const currentWorkspaceIdResp = await this.http.get(this.getPath(['_current'])); + if (currentWorkspaceIdResp.success) { + if (!currentWorkspaceIdResp.result) { + return { + success: false, + error: 'You are not in any workspace yet.', + }; + } + } + + return currentWorkspaceIdResp; } public async getCurrentWorkspace(): Promise> { - return { - success: false, - error: 'Unimplement', - }; + const currentWorkspaceIdResp = await this.getCurrentWorkspaceId(); + if (currentWorkspaceIdResp.success) { + const currentWorkspaceResp = await this.get(currentWorkspaceIdResp.result); + return currentWorkspaceResp; + } else { + return { + success: false, + error: currentWorkspaceIdResp.error || '', + }; + } } /** diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts index dbd7b20809e2..6a0422e2aa30 100644 --- a/src/core/server/workspaces/routes/index.ts +++ b/src/core/server/workspaces/routes/index.ts @@ -5,7 +5,13 @@ import { schema } from '@osd/config-schema'; import { InternalHttpServiceSetup } from '../../http'; import { Logger } from '../../logging'; -import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL } from '../types'; +import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL, WORKSPACE_ID_COOKIE_NAME } from '../types'; + +function getCookieValue(cookieString: string, cookieName: string): string | null { + const regex = new RegExp(`(?:(?:^|.*;\\s*)${cookieName}\\s*\\=\\s*([^;]*).*$)|^.*$`); + const match = cookieString.match(regex); + return match ? match[1] : null; +} export function registerRoutes({ client, @@ -143,4 +149,72 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); + router.post( + { + path: '/_enter/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + + const result = await client.get( + { + context, + request: req, + logger, + }, + id + ); + if (result.success) { + return res.custom({ + body: { + success: true, + }, + statusCode: 200, + headers: { + 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=${id}; Path=/`, + }, + }); + } else { + return res.ok({ body: result }); + } + }) + ); + + router.post( + { + path: '/_exit', + validate: {}, + }, + router.handleLegacyErrors(async (context, req, res) => { + return res.custom({ + body: { + success: true, + }, + statusCode: 200, + headers: { + 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/`, + }, + }); + }) + ); + + router.get( + { + path: '/_current', + validate: {}, + }, + router.handleLegacyErrors(async (context, req, res) => { + return res.ok({ + body: { + success: true, + result: getCookieValue(req.headers.cookie as string, WORKSPACE_ID_COOKIE_NAME), + }, + }); + }) + ); } diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts index cc24797605e2..fc3cae861c77 100644 --- a/src/core/server/workspaces/types.ts +++ b/src/core/server/workspaces/types.ts @@ -69,3 +69,5 @@ export type IResponse = }; export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export const WORKSPACE_ID_COOKIE_NAME = 'trinity_workspace_id'; From 1d5deb5770b12323fc01f69ea822f5bc52af2d66 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Tue, 13 Jun 2023 15:14:23 +0800 Subject: [PATCH 15/54] feat: implement more for workspaces service Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/workspaces_client.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index e275166f9139..6b369517b366 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -57,13 +57,11 @@ export class WorkspacesClient { public async getCurrentWorkspaceId(): Promise> { const currentWorkspaceIdResp = await this.http.get(this.getPath(['_current'])); - if (currentWorkspaceIdResp.success) { - if (!currentWorkspaceIdResp.result) { - return { - success: false, - error: 'You are not in any workspace yet.', - }; - } + if (currentWorkspaceIdResp.success && !currentWorkspaceIdResp.result) { + return { + success: false, + error: 'You are not in any workspace yet.', + }; } return currentWorkspaceIdResp; @@ -75,10 +73,7 @@ export class WorkspacesClient { const currentWorkspaceResp = await this.get(currentWorkspaceIdResp.result); return currentWorkspaceResp; } else { - return { - success: false, - error: currentWorkspaceIdResp.error || '', - }; + return currentWorkspaceIdResp; } } From 666bb2f0981a2561e8ee547cf8c804d8e948cdb3 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Tue, 13 Jun 2023 17:54:57 +0800 Subject: [PATCH 16/54] feat: implement more for workspaces service Signed-off-by: SuZhoue-Joe --- src/core/public/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 6a0b344acdc8..ab8a70b7c1fd 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -346,3 +346,12 @@ export { }; export { __osdBootstrap__ } from './osd_bootstrap'; + +export { + WorkspacesClientContract, + WorkspacesClient, + WorkspacesStart, + WorkspacesService, + WorkspaceAttribute, + WorkspaceFindOptions, +} from './workspace'; From e3e278c305e6c47b4a94c31bd5fa8a657cbe58d6 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 14 Jun 2023 14:43:01 +0800 Subject: [PATCH 17/54] feat: add workspace creator page (#5) * feat: add workspace creator page Signed-off-by: Lin Wang * feat: integrate with application workspace template Signed-off-by: Lin Wang * feat: add max-width and remove image wrapper if not exists Signed-off-by: Lin Wang * feat: update filter condition to align with collapsible nav Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang --- .../components/workspace_creator/index.tsx | 6 +- .../workspace_creator/workspace_creator.tsx | 39 ++ .../workspace_creator/workspace_form.tsx | 338 ++++++++++++++++++ src/plugins/workspace/public/hooks.ts | 11 + 4 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx create mode 100644 src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx diff --git a/src/plugins/workspace/public/components/workspace_creator/index.tsx b/src/plugins/workspace/public/components/workspace_creator/index.tsx index 11a3e0feaf92..c8cdbfab65be 100644 --- a/src/plugins/workspace/public/components/workspace_creator/index.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/index.tsx @@ -3,8 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; - -export const WorkspaceCreator = () => { - return
TODO
; -}; +export { WorkspaceCreator } from './workspace_creator'; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx new file mode 100644 index 000000000000..e7006464ba7d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; + +import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; + +import { WorkspaceForm } from './workspace_form'; + +export const WorkspaceCreator = () => { + const { + services: { application }, + } = useOpenSearchDashboards(); + + const handleWorkspaceFormSubmit = useCallback(() => {}, []); + + return ( + + + + + {application && ( + + )} + + + + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx new file mode 100644 index 000000000000..8cb3a1e3c39d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -0,0 +1,338 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useState, FormEventHandler, useRef, useMemo } from 'react'; +import { groupBy } from 'lodash'; +import { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiText, + EuiButton, + EuiFlexItem, + EuiCheckableCard, + htmlIdGenerator, + EuiFlexGrid, + EuiFlexGroup, + EuiImage, + EuiAccordion, + EuiCheckbox, + EuiCheckboxGroup, + EuiCheckableCardProps, + EuiCheckboxGroupProps, + EuiCheckboxProps, + EuiFieldTextProps, +} from '@elastic/eui'; + +import { WorkspaceTemplate } from '../../../../../core/types'; +import { AppNavLinkStatus, ApplicationStart } from '../../../../../core/public'; +import { useApplications, useWorkspaceTemplate } from '../../hooks'; + +interface WorkspaceFeature { + id: string; + name: string; + templates: WorkspaceTemplate[]; +} + +interface WorkspaceFeatureGroup { + name: string; + features: WorkspaceFeature[]; +} + +interface WorkspaceFormData { + name: string; + description?: string; + features: string[]; +} + +type WorkspaceFormErrors = { [key in keyof WorkspaceFormData]?: string }; + +const isWorkspaceFeatureGroup = ( + featureOrGroup: WorkspaceFeature | WorkspaceFeatureGroup +): featureOrGroup is WorkspaceFeatureGroup => 'features' in featureOrGroup; + +const workspaceHtmlIdGenerator = htmlIdGenerator(); + +interface WorkspaceFormProps { + application: ApplicationStart; + onSubmit?: (formData: WorkspaceFormData) => void; + defaultValues?: WorkspaceFormData; +} +export const WorkspaceForm = ({ application, onSubmit, defaultValues }: WorkspaceFormProps) => { + const { workspaceTemplates, templateFeatureMap } = useWorkspaceTemplate(application); + const applications = useApplications(application); + + const [name, setName] = useState(defaultValues?.name); + const [description, setDescription] = useState(defaultValues?.description); + const [selectedTemplateId, setSelectedTemplateId] = useState(); + const [selectedFeatureIds, setSelectedFeatureIds] = useState(defaultValues?.features || []); + const selectedTemplate = workspaceTemplates.find( + (template) => template.id === selectedTemplateId + ); + const [formErrors, setFormErrors] = useState({}); + const formIdRef = useRef(); + const getFormData = () => ({ + name, + description, + features: selectedFeatureIds, + }); + const getFormDataRef = useRef(getFormData); + getFormDataRef.current = getFormData; + + const featureOrGroups = useMemo(() => { + const category2Applications = groupBy(applications, 'category.label'); + return Object.keys(category2Applications).reduce< + Array + >((previousValue, currentKey) => { + const apps = category2Applications[currentKey]; + const features = apps + .filter( + ({ navLinkStatus, chromeless }) => + navLinkStatus !== AppNavLinkStatus.hidden && !chromeless + ) + .map(({ id, title, workspaceTemplate }) => ({ + id, + name: title, + templates: workspaceTemplate || [], + })); + if (features.length === 1 || currentKey === 'undefined') { + return [...previousValue, ...features]; + } + return [ + ...previousValue, + { + name: apps[0].category?.label || '', + features, + }, + ]; + }, []); + }, [applications]); + + if (!formIdRef.current) { + formIdRef.current = workspaceHtmlIdGenerator(); + } + + const handleTemplateCardChange = useCallback( + (e) => { + const templateId = e.target.value; + setSelectedTemplateId(templateId); + setSelectedFeatureIds( + featureOrGroups.reduce( + (previousData, currentData) => [ + ...previousData, + ...(isWorkspaceFeatureGroup(currentData) ? currentData.features : [currentData]) + .filter(({ templates }) => !!templates.find((template) => template.id === templateId)) + .map((feature) => feature.id), + ], + [] + ) + ); + }, + [featureOrGroups] + ); + + const handleFeatureChange = useCallback((featureId) => { + setSelectedFeatureIds((previousData) => + previousData.includes(featureId) + ? previousData.filter((id) => id !== featureId) + : [...previousData, featureId] + ); + }, []); + + const handleFeatureCheckboxChange = useCallback( + (e) => { + handleFeatureChange(e.target.id); + }, + [handleFeatureChange] + ); + + const handleFeatureGroupChange = useCallback( + (e) => { + for (const featureOrGroup of featureOrGroups) { + if (isWorkspaceFeatureGroup(featureOrGroup) && featureOrGroup.name === e.target.id) { + const groupFeatureIds = featureOrGroup.features.map((feature) => feature.id); + setSelectedFeatureIds((previousData) => { + const notExistsIds = groupFeatureIds.filter((id) => !previousData.includes(id)); + if (notExistsIds.length > 0) { + return [...previousData, ...notExistsIds]; + } + return previousData.filter((id) => !groupFeatureIds.includes(id)); + }); + } + } + }, + [featureOrGroups] + ); + + const handleFormSubmit = useCallback( + (e) => { + e.preventDefault(); + const formData = getFormDataRef.current(); + if (!formData.name) { + setFormErrors({ name: "Name can't be empty." }); + return; + } + setFormErrors({}); + onSubmit?.({ ...formData, name: formData.name }); + }, + [onSubmit] + ); + + const handleNameInputChange = useCallback['onChange']>((e) => { + setName(e.target.value); + }, []); + + const handleDescriptionInputChange = useCallback['onChange']>((e) => { + setDescription(e.target.value); + }, []); + + return ( + + + +

Workspace details

+
+ + + + + + Description - optional + + } + > + + +
+ + + +

Workspace Template

+
+ + + {workspaceTemplates.map((template) => ( + + + + ))} + + + {selectedTemplate && ( + <> + +

Features

+
+ + + {selectedTemplate.coverImage && ( + + + + )} + + {selectedTemplate.description} + +

Key Features:

+
+ + + {templateFeatureMap.get(selectedTemplate.id)?.map((feature) => ( + • {feature.title} + ))} + +
+
+ + + )} + + +

Advanced Options

+
+ + } + > + + {featureOrGroups.map((featureOrGroup) => { + const features = isWorkspaceFeatureGroup(featureOrGroup) + ? featureOrGroup.features + : []; + const selectedIds = selectedFeatureIds.filter((id) => + (isWorkspaceFeatureGroup(featureOrGroup) + ? featureOrGroup.features + : [featureOrGroup] + ).find((item) => item.id === id) + ); + return ( + + 0 ? `(${selectedIds.length}/${features.length})` : '' + }`} + checked={selectedIds.length > 0} + indeterminate={ + isWorkspaceFeatureGroup(featureOrGroup) && + selectedIds.length > 0 && + selectedIds.length < features.length + } + /> + {isWorkspaceFeatureGroup(featureOrGroup) && ( + ({ + id: item.id, + label: item.name, + }))} + idToSelectedMap={selectedIds.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue]: true, + }), + {} + )} + onChange={handleFeatureChange} + style={{ marginLeft: 40 }} + /> + )} + + ); + })} + +
+
+ + + + Create workspace + + +
+ ); +}; diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 71019c5948a2..636a00742146 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -8,6 +8,17 @@ import { useObservable } from 'react-use'; import { useMemo } from 'react'; import { WorkspaceTemplate } from '../../../core/types'; +export function useApplications(application: ApplicationStart) { + const applications = useObservable(application.applications$); + return useMemo(() => { + const apps: PublicAppInfo[] = []; + applications?.forEach((app) => { + apps.push(app); + }); + return apps; + }, [applications]); +} + export function useWorkspaceTemplate(application: ApplicationStart) { const applications = useObservable(application.applications$); From 9be35b03916d882c039fa6ad4113f10d005cb11f Mon Sep 17 00:00:00 2001 From: suzhou Date: Thu, 15 Jun 2023 10:28:09 +0800 Subject: [PATCH 18/54] Add validation when load page (#8) * fix: validation & query Signed-off-by: SuZhoue-Joe * feat: modify file name to reduce confusion Signed-off-by: SuZhoue-Joe * feat: add landing logic to retrive workspace id Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * fix: type error Signed-off-by: SuZhoue-Joe * fix: type error Signed-off-by: SuZhoue-Joe * feat: make client more robust Signed-off-by: SuZhoue-Joe * feat: use Subject Signed-off-by: SuZhoue-Joe --------- Signed-off-by: SuZhoue-Joe --- src/core/public/core_app/core_app.ts | 2 + src/core/public/core_system.ts | 6 +- .../fatal_errors/fatal_errors_service.mock.ts | 30 +++ src/core/public/index.ts | 5 +- src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 7 +- src/core/public/workspace/consts.ts | 8 + src/core/public/workspace/index.ts | 5 +- .../public/workspace/workspaces_client.ts | 173 +++++++++++++++--- .../public/workspace/workspaces_service.ts | 49 ++++- .../integration_tests/core_services.test.ts | 4 +- src/core/server/internal_types.ts | 3 - src/core/server/server.ts | 6 +- src/core/server/workspaces/index.ts | 2 +- src/core/server/workspaces/routes/index.ts | 96 ++-------- .../workspaces/saved_objects/workspace.ts | 4 +- src/core/server/workspaces/types.ts | 12 +- ...h_saved_object.ts => workspaces_client.ts} | 0 .../server/workspaces/workspaces_service.ts | 12 +- src/plugins/workspace/common/constants.ts | 1 + src/plugins/workspace/public/plugin.ts | 66 ++++++- 21 files changed, 340 insertions(+), 152 deletions(-) create mode 100644 src/core/public/workspace/consts.ts rename src/core/server/workspaces/{workspaces_client_with_saved_object.ts => workspaces_client.ts} (100%) diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index fcbcc5de5655..c4d359d58dc1 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -42,12 +42,14 @@ import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; import { renderApp as renderStatusApp } from './status'; +import { WorkspacesSetup } from '../workspace'; interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; + workspaces: WorkspacesSetup; } interface StartDeps { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 27f39ea57c1b..1c0286cbd03c 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -163,13 +163,14 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); + const workspaces = await this.workspaces.setup({ http }); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies: new Map([...pluginDependencies]), }); const application = this.application.setup({ context, http }); - this.coreApp.setup({ application, http, injectedMetadata, notifications }); + this.coreApp.setup({ application, http, injectedMetadata, notifications, workspaces }); const core: InternalCoreSetup = { application, @@ -179,6 +180,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }; // Services that do not expose contracts at setup @@ -202,7 +204,7 @@ export class CoreSystem { const uiSettings = await this.uiSettings.start(); const docLinks = this.docLinks.start({ injectedMetadata }); const http = await this.http.start(); - const workspaces = await this.workspaces.start({ http }); + const workspaces = await this.workspaces.start(); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const fatalErrors = await this.fatalErrors.start(); diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index ff1b252fc128..5079fc8f4b6a 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -30,6 +30,8 @@ import type { PublicMethodsOf } from '@osd/utility-types'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors_service'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { WorkspaceAttribute } from '../workspace'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { @@ -58,3 +60,31 @@ export const fatalErrorsServiceMock = { createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; + +const currentWorkspaceId$ = new BehaviorSubject(''); +const workspaceList$ = new Subject(); + +const createWorkspacesSetupContractMock = () => ({ + client: { + currentWorkspaceId$, + workspaceList$, + stop: jest.fn(), + enterWorkspace: jest.fn(), + exitWorkspace: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + list: jest.fn(), + getCurrentWorkspace: jest.fn(), + getCurrentWorkspaceId: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + formatUrlWithWorkspaceId: jest.fn(), +}); + +const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; + +export const workspacesServiceMock = { + createSetupContractMock: createWorkspacesStartContractMock, + createStartContract: createWorkspacesStartContractMock, +}; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ab8a70b7c1fd..236048a012e7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -88,7 +88,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; -import { WorkspacesStart } from './workspace'; +import { WorkspacesStart, WorkspacesSetup } from './workspace'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ @@ -241,6 +241,8 @@ export interface CoreSetup; + /** {@link WorkspacesSetup} */ + workspaces: WorkspacesSetup; } /** @@ -354,4 +356,5 @@ export { WorkspacesService, WorkspaceAttribute, WorkspaceFindOptions, + WORKSPACE_ID_QUERYSTRING_NAME, } from './workspace'; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 81fcb2b34dab..87738fc7e57a 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -121,6 +121,7 @@ export function createPluginSetupContext< getBranding: deps.injectedMetadata.getBranding, }, getStartServices: () => plugin.startDependencies, + workspaces: deps.workspaces, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 5fa3bf888b5c..9c36a791332d 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -50,7 +50,10 @@ import { applicationServiceMock } from '../application/application_service.mock' import { i18nServiceMock } from '../i18n/i18n_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { + fatalErrorsServiceMock, + workspacesServiceMock, +} from '../fatal_errors/fatal_errors_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -108,6 +111,7 @@ describe('PluginsService', () => { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + workspaces: workspacesServiceMock.createSetupContractMock(), }; mockSetupContext = { ...mockSetupDeps, @@ -127,6 +131,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts new file mode 100644 index 000000000000..77f9144a27a3 --- /dev/null +++ b/src/core/public/workspace/consts.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index a70d91733ab6..d0fb17ead0c1 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -3,5 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ export { WorkspacesClientContract, WorkspacesClient } from './workspaces_client'; -export { WorkspacesStart, WorkspacesService } from './workspaces_service'; -export { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; +export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; +export type { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; +export { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 6b369517b366..8508921cbb0d 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -3,17 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ import { resolve as resolveUrl } from 'url'; -import type { PublicMethodsOf } from '@osd/utility-types'; -import { WORKSPACES_API_BASE_URL } from '../../server/types'; -import { HttpStart } from '../http'; +import type { PublicContract } from '@osd/utility-types'; +import { Subject } from 'rxjs'; +import { HttpFetchError, HttpFetchOptions, HttpSetup } from '../http'; import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; +import { WORKSPACES_API_BASE_URL } from './consts'; /** * WorkspacesClientContract as implemented by the {@link WorkspacesClient} * * @public */ -export type WorkspacesClientContract = PublicMethodsOf; +export type WorkspacesClientContract = PublicContract; const join = (...uriComponents: Array) => uriComponents @@ -38,33 +39,107 @@ type IResponse = * @public */ export class WorkspacesClient { - private http: HttpStart; - constructor(http: HttpStart) { + private http: HttpSetup; + private currentWorkspaceId = ''; + public currentWorkspaceId$ = new Subject(); + public workspaceList$ = new Subject(); + constructor(http: HttpSetup) { this.http = http; + this.currentWorkspaceId$.subscribe( + (currentWorkspaceId) => (this.currentWorkspaceId = currentWorkspaceId) + ); + /** + * Add logic to check if current workspace id is still valid + * If not, remove the current workspace id and notify other subscribers + */ + this.workspaceList$.subscribe(async (workspaceList) => { + const currentWorkspaceId = this.currentWorkspaceId; + if (currentWorkspaceId) { + const findItem = workspaceList.find((item) => item.id === currentWorkspaceId); + if (!findItem) { + /** + * Current workspace is staled + */ + this.currentWorkspaceId$.next(''); + } + } + }); + + /** + * Initialize workspace list + */ + this.updateWorkspaceListAndNotify(); } + private catchedFetch = async >( + path: string, + options: HttpFetchOptions + ) => { + try { + return await this.http.fetch(path, options); + } catch (error: unknown) { + if (error instanceof HttpFetchError || error instanceof Error) { + return { + success: false, + error: error.message, + } as T; + } + + return { + success: false, + error: 'Unknown error', + } as T; + } + }; + private getPath(path: Array): string { return resolveUrl(`${WORKSPACES_API_BASE_URL}/`, join(...path)); } + private async updateWorkspaceListAndNotify(): Promise { + const result = await this.list({ + perPage: 999, + }); + + if (result?.success) { + this.workspaceList$.next(result.result.workspaces); + } + } + public async enterWorkspace(id: string): Promise> { - return this.http.post(this.getPath(['_enter', id])); + const workspaceResp = await this.get(id); + if (workspaceResp.success) { + this.currentWorkspaceId$.next(id); + return { + success: true, + result: null, + }; + } else { + return workspaceResp; + } } public async exitWorkspace(): Promise> { - return this.http.post(this.getPath(['_exit'])); + this.currentWorkspaceId$.next(''); + return { + success: true, + result: null, + }; } public async getCurrentWorkspaceId(): Promise> { - const currentWorkspaceIdResp = await this.http.get(this.getPath(['_current'])); - if (currentWorkspaceIdResp.success && !currentWorkspaceIdResp.result) { + const currentWorkspaceId = this.currentWorkspaceId; + if (!currentWorkspaceId) { return { success: false, error: 'You are not in any workspace yet.', }; } - return currentWorkspaceIdResp; + return { + success: true, + result: currentWorkspaceId, + }; } public async getCurrentWorkspace(): Promise> { @@ -83,22 +158,31 @@ export class WorkspacesClient { * @param attributes * @returns */ - public create = ( + public async create( attributes: Omit - ): Promise> => { + ): Promise> { if (!attributes) { - return Promise.reject(new Error('requires attributes')); + return { + success: false, + error: 'Workspace attributes is required', + }; } const path = this.getPath([]); - return this.http.fetch(path, { + const result = await this.catchedFetch>(path, { method: 'POST', body: JSON.stringify({ attributes, }), }); - }; + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } /** * Deletes a workspace @@ -106,13 +190,22 @@ export class WorkspacesClient { * @param id * @returns */ - public delete = (id: string): Promise> => { + public async delete(id: string): Promise> { if (!id) { - return Promise.reject(new Error('requires id')); + return { + success: false, + error: 'Id is required.', + }; } - return this.http.delete(this.getPath([id]), { method: 'DELETE' }); - }; + const result = await this.catchedFetch(this.getPath([id]), { method: 'DELETE' }); + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } /** * Search for workspaces @@ -137,9 +230,9 @@ export class WorkspacesClient { }> > => { const path = this.getPath(['_list']); - return this.http.fetch(path, { - method: 'GET', - query: options, + return this.catchedFetch(path, { + method: 'POST', + body: JSON.stringify(options || {}), }); }; @@ -149,16 +242,19 @@ export class WorkspacesClient { * @param {string} id * @returns The workspace for the given id. */ - public get = (id: string): Promise> => { + public async get(id: string): Promise> { if (!id) { - return Promise.reject(new Error('requires id')); + return { + success: false, + error: 'Id is required.', + }; } const path = this.getPath([id]); - return this.http.fetch(path, { + return this.catchedFetch(path, { method: 'GET', }); - }; + } /** * Updates a workspace @@ -167,9 +263,15 @@ export class WorkspacesClient { * @param {object} attributes * @returns */ - public update(id: string, attributes: Partial): Promise> { + public async update( + id: string, + attributes: Partial + ): Promise> { if (!id || !attributes) { - return Promise.reject(new Error('requires id and attributes')); + return { + success: false, + error: 'Id and attributes are required.', + }; } const path = this.getPath([id]); @@ -177,9 +279,20 @@ export class WorkspacesClient { attributes, }; - return this.http.fetch(path, { + const result = await this.catchedFetch(path, { method: 'PUT', body: JSON.stringify(body), }); + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } + + public stop() { + this.workspaceList$.unsubscribe(); + this.currentWorkspaceId$.unsubscribe(); } } diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index f691ebbe3e16..908530885760 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -4,19 +4,56 @@ */ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; -import { HttpStart } from '..'; +import type { WorkspaceAttribute } from '../../server/types'; +import { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; +import { HttpSetup } from '../http'; /** * @public */ export interface WorkspacesStart { client: WorkspacesClientContract; + formatUrlWithWorkspaceId: (url: string, id: WorkspaceAttribute['id']) => string; } -export class WorkspacesService implements CoreService { - public async setup() {} - public async start({ http }: { http: HttpStart }): Promise { - return { client: new WorkspacesClient(http) }; +export type WorkspacesSetup = WorkspacesStart; + +function setQuerystring(url: string, params: Record): string { + const urlObj = new URL(url); + const searchParams = new URLSearchParams(urlObj.search); + + for (const key in params) { + if (params.hasOwnProperty(key)) { + const value = params[key]; + searchParams.set(key, value); + } + } + + urlObj.search = searchParams.toString(); + return urlObj.toString(); +} + +export class WorkspacesService implements CoreService { + private client?: WorkspacesClientContract; + private formatUrlWithWorkspaceId(url: string, id: string) { + return setQuerystring(url, { + [WORKSPACE_ID_QUERYSTRING_NAME]: id, + }); + } + public async setup({ http }: { http: HttpSetup }) { + this.client = new WorkspacesClient(http); + return { + client: this.client, + formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + }; + } + public async start(): Promise { + return { + client: this.client as WorkspacesClientContract, + formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + }; + } + public async stop() { + this.client?.stop(); } - public async stop() {} } diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index c489d98cf708..b248a67ef50c 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -520,7 +520,7 @@ describe('http service', () => { }); const coreStart = await root.start(); - opensearch = coreStart.opensearch; + opensearch = coreStart?.opensearch; const { header } = await osdTestServer.request.get(root, '/new-platform/').expect(401); @@ -556,7 +556,7 @@ describe('http service', () => { }); const coreStart = await root.start(); - opensearch = coreStart.opensearch; + opensearch = coreStart?.opensearch; const { header } = await osdTestServer.request.get(root, '/new-platform/').expect(401); diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 0eb0b684d4f4..2b4df7da68bf 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -48,7 +48,6 @@ import { InternalStatusServiceSetup } from './status'; import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; -import { InternalWorkspacesServiceSetup, InternalWorkspacesServiceStart } from './workspaces'; /** @internal */ export interface InternalCoreSetup { @@ -65,7 +64,6 @@ export interface InternalCoreSetup { auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; - workspaces: InternalWorkspacesServiceSetup; } /** @@ -80,7 +78,6 @@ export interface InternalCoreStart { uiSettings: InternalUiSettingsServiceStart; auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; - workspaces: InternalWorkspacesServiceStart; } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 99c79351d861..f80b90ba6baa 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -175,7 +175,7 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); - const workspacesSetup = await this.workspaces.setup({ + await this.workspaces.setup({ http: httpSetup, savedObject: savedObjectsSetup, }); @@ -220,7 +220,6 @@ export class Server { auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, - workspaces: workspacesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -262,7 +261,7 @@ export class Server { opensearch: opensearchStart, savedObjects: savedObjectsStart, }); - const workspacesStart = await this.workspaces.start({ + await this.workspaces.start({ savedObjects: savedObjectsStart, }); @@ -275,7 +274,6 @@ export class Server { uiSettings: uiSettingsStart, auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, - workspaces: workspacesStart, }; const pluginsStart = await this.plugins.start(this.coreStart); diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts index 838f216bbd86..b9f765e4bba3 100644 --- a/src/core/server/workspaces/index.ts +++ b/src/core/server/workspaces/index.ts @@ -10,4 +10,4 @@ export { InternalWorkspacesServiceStart, } from './workspaces_service'; -export { WorkspaceAttribute, WorkspaceFindOptions, WORKSPACES_API_BASE_URL } from './types'; +export { WorkspaceAttribute, WorkspaceFindOptions } from './types'; diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts index 6a0422e2aa30..24345b6a34d9 100644 --- a/src/core/server/workspaces/routes/index.ts +++ b/src/core/server/workspaces/routes/index.ts @@ -5,13 +5,11 @@ import { schema } from '@osd/config-schema'; import { InternalHttpServiceSetup } from '../../http'; import { Logger } from '../../logging'; -import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL, WORKSPACE_ID_COOKIE_NAME } from '../types'; +import { IWorkspaceDBImpl } from '../types'; -function getCookieValue(cookieString: string, cookieName: string): string | null { - const regex = new RegExp(`(?:(?:^|.*;\\s*)${cookieName}\\s*\\=\\s*([^;]*).*$)|^.*$`); - const match = cookieString.match(regex); - return match ? match[1] : null; -} +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; export function registerRoutes({ client, @@ -23,15 +21,17 @@ export function registerRoutes({ http: InternalHttpServiceSetup; }) { const router = http.createRouter(WORKSPACES_API_BASE_URL); - router.get( + router.post( { path: '/_list', validate: { - query: schema.object({ - per_page: schema.number({ min: 0, defaultValue: 20 }), + body: schema.object({ + search: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.string()), + perPage: schema.number({ min: 0, defaultValue: 20 }), page: schema.number({ min: 0, defaultValue: 1 }), - sort_field: schema.maybe(schema.string()), - fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + sortField: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), }), }, }, @@ -42,7 +42,7 @@ export function registerRoutes({ request: req, logger, }, - req.query + req.body ); return res.ok({ body: result }); }) @@ -71,12 +71,13 @@ export function registerRoutes({ ); router.post( { - path: '/{id?}', + path: '/', validate: { body: schema.object({ attributes: schema.object({ description: schema.maybe(schema.string()), name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), }), }), }, @@ -106,6 +107,7 @@ export function registerRoutes({ attributes: schema.object({ description: schema.maybe(schema.string()), name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), }), }), }, @@ -149,72 +151,4 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); - router.post( - { - path: '/_enter/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - router.handleLegacyErrors(async (context, req, res) => { - const { id } = req.params; - - const result = await client.get( - { - context, - request: req, - logger, - }, - id - ); - if (result.success) { - return res.custom({ - body: { - success: true, - }, - statusCode: 200, - headers: { - 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=${id}; Path=/`, - }, - }); - } else { - return res.ok({ body: result }); - } - }) - ); - - router.post( - { - path: '/_exit', - validate: {}, - }, - router.handleLegacyErrors(async (context, req, res) => { - return res.custom({ - body: { - success: true, - }, - statusCode: 200, - headers: { - 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/`, - }, - }); - }) - ); - - router.get( - { - path: '/_current', - validate: {}, - }, - router.handleLegacyErrors(async (context, req, res) => { - return res.ok({ - body: { - success: true, - result: getCookieValue(req.headers.cookie as string, WORKSPACE_ID_COOKIE_NAME), - }, - }); - }) - ); } diff --git a/src/core/server/workspaces/saved_objects/workspace.ts b/src/core/server/workspaces/saved_objects/workspace.ts index d211aaa3ea93..e3fbaa0dad6a 100644 --- a/src/core/server/workspaces/saved_objects/workspace.ts +++ b/src/core/server/workspaces/saved_objects/workspace.ts @@ -29,8 +29,8 @@ export const workspace: SavedObjectsType = { mappings: { dynamic: false, properties: { - title: { - type: 'text', + name: { + type: 'keyword', }, description: { type: 'text', diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts index fc3cae861c77..e098b4905a1f 100644 --- a/src/core/server/workspaces/types.ts +++ b/src/core/server/workspaces/types.ts @@ -19,11 +19,11 @@ export interface WorkspaceAttribute { export interface WorkspaceFindOptions { page?: number; - per_page?: number; + perPage?: number; search?: string; - search_fields?: string[]; - sort_field?: string; - sort_order?: string; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; } export interface IRequestDetail { @@ -67,7 +67,3 @@ export type IResponse = success: false; error?: string; }; - -export const WORKSPACES_API_BASE_URL = '/api/workspaces'; - -export const WORKSPACE_ID_COOKIE_NAME = 'trinity_workspace_id'; diff --git a/src/core/server/workspaces/workspaces_client_with_saved_object.ts b/src/core/server/workspaces/workspaces_client.ts similarity index 100% rename from src/core/server/workspaces/workspaces_client_with_saved_object.ts rename to src/core/server/workspaces/workspaces_client.ts diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 15c150b7378a..6faec9a6496e 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -12,10 +12,10 @@ import { InternalSavedObjectsServiceStart, } from '../saved_objects'; import { IWorkspaceDBImpl } from './types'; -import { WorkspacesClientWithSavedObject } from './workspaces_client_with_saved_object'; +import { WorkspacesClientWithSavedObject } from './workspaces_client'; export interface WorkspacesServiceSetup { - setWorkspacesClient: (client: IWorkspaceDBImpl) => void; + client: IWorkspaceDBImpl; } export interface WorkspacesServiceStart { @@ -39,14 +39,14 @@ export class WorkspacesService implements CoreService { private logger: Logger; private client?: IWorkspaceDBImpl; - constructor(private readonly coreContext: CoreContext) { + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); } public async setup(setupDeps: WorkspacesSetupDeps): Promise { this.logger.debug('Setting up Workspaces service'); - this.client = this.client || new WorkspacesClientWithSavedObject(setupDeps); + this.client = new WorkspacesClientWithSavedObject(setupDeps); await this.client.setup(setupDeps); registerRoutes({ @@ -56,9 +56,7 @@ export class WorkspacesService }); return { - setWorkspacesClient: (client: IWorkspaceDBImpl) => { - this.client = client; - }, + client: this.client, }; } diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 5ccad2c6a2a9..4ac1575c25f7 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -5,3 +5,4 @@ export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; +export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 13017dab8835..911c9a651137 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -4,6 +4,7 @@ */ import { i18n } from '@osd/i18n'; +import { parse } from 'querystring'; import { CoreSetup, CoreStart, @@ -11,10 +12,71 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID } from '../common/constants'; +import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../common/constants'; +import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; export class WorkspacesPlugin implements Plugin<{}, {}> { - public setup(core: CoreSetup) { + private core?: CoreSetup; + private addWorkspaceListener() { + this.core?.workspaces.client.currentWorkspaceId$.subscribe((newWorkspaceId) => { + try { + sessionStorage.setItem(WORKSPACE_ID_IN_SESSION_STORAGE, newWorkspaceId); + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + } + }); + } + private getWorkpsaceIdFromQueryString(): string { + const querystringObject = parse(window.location.search.replace(/^\??/, '')); + return querystringObject[WORKSPACE_ID_QUERYSTRING_NAME] as string; + } + private getWorkpsaceIdFromSessionStorage(): string { + try { + return sessionStorage.getItem(WORKSPACE_ID_IN_SESSION_STORAGE) || ''; + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + return ''; + } + } + private clearWorkspaceIdFromSessionStorage(): void { + try { + sessionStorage.removeItem(WORKSPACE_ID_IN_SESSION_STORAGE); + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + } + } + public async setup(core: CoreSetup) { + this.core = core; + /** + * register a listener + */ + this.addWorkspaceListener(); + + /** + * Retrive workspace id from url or sessionstorage + * url > sessionstorage + */ + const workspaceId = + this.getWorkpsaceIdFromQueryString() || this.getWorkpsaceIdFromSessionStorage(); + + if (workspaceId) { + const result = await core.workspaces.client.enterWorkspace(workspaceId); + if (!result.success) { + this.clearWorkspaceIdFromSessionStorage(); + core.fatalErrors.add( + result.error || + i18n.translate('workspace.error.setup', { + defaultMessage: 'Workspace init failed', + }) + ); + } + } core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', { From 169ed8a77cb658b8f55d5269b46964b9daef51f6 Mon Sep 17 00:00:00 2001 From: suzhou Date: Thu, 15 Jun 2023 15:44:29 +0800 Subject: [PATCH 19/54] feat: use BehaviorObject and optimize code (#14) Signed-off-by: SuZhoue-Joe --- src/core/public/workspace/consts.ts | 4 + .../public/workspace/workspaces_client.ts | 83 +++++++------------ src/core/server/workspaces/routes/index.ts | 10 +-- src/plugins/workspace/public/plugin.ts | 18 ++-- 4 files changed, 48 insertions(+), 67 deletions(-) diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts index 77f9144a27a3..662baeaa5d19 100644 --- a/src/core/public/workspace/consts.ts +++ b/src/core/public/workspace/consts.ts @@ -6,3 +6,7 @@ export const WORKSPACES_API_BASE_URL = '/api/workspaces'; export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; + +export enum WORKSPACE_ERROR_REASON_MAP { + WORKSPACE_STALED = 'WORKSPACE_STALED', +} diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 8508921cbb0d..91d83dd1639f 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -2,12 +2,11 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { resolve as resolveUrl } from 'url'; import type { PublicContract } from '@osd/utility-types'; -import { Subject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { HttpFetchError, HttpFetchOptions, HttpSetup } from '../http'; import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; -import { WORKSPACES_API_BASE_URL } from './consts'; +import { WORKSPACES_API_BASE_URL, WORKSPACE_ERROR_REASON_MAP } from './consts'; /** * WorkspacesClientContract as implemented by the {@link WorkspacesClient} @@ -40,27 +39,25 @@ type IResponse = */ export class WorkspacesClient { private http: HttpSetup; - private currentWorkspaceId = ''; - public currentWorkspaceId$ = new Subject(); - public workspaceList$ = new Subject(); + public currentWorkspaceId$ = new BehaviorSubject(''); + public workspaceList$ = new BehaviorSubject([]); constructor(http: HttpSetup) { this.http = http; - this.currentWorkspaceId$.subscribe( - (currentWorkspaceId) => (this.currentWorkspaceId = currentWorkspaceId) - ); /** * Add logic to check if current workspace id is still valid * If not, remove the current workspace id and notify other subscribers */ this.workspaceList$.subscribe(async (workspaceList) => { - const currentWorkspaceId = this.currentWorkspaceId; + const currentWorkspaceId = this.currentWorkspaceId$.getValue(); if (currentWorkspaceId) { const findItem = workspaceList.find((item) => item.id === currentWorkspaceId); if (!findItem) { /** * Current workspace is staled */ - this.currentWorkspaceId$.next(''); + this.currentWorkspaceId$.error({ + reason: WORKSPACE_ERROR_REASON_MAP.WORKSPACE_STALED, + }); } } }); @@ -71,29 +68,39 @@ export class WorkspacesClient { this.updateWorkspaceListAndNotify(); } - private catchedFetch = async >( + /** + * Add a non-throw-error fetch method for internal use. + */ + private safeFetch = async ( path: string, options: HttpFetchOptions - ) => { + ): Promise> => { try { - return await this.http.fetch(path, options); + return await this.http.fetch>(path, options); } catch (error: unknown) { - if (error instanceof HttpFetchError || error instanceof Error) { + if (error instanceof HttpFetchError) { + return { + success: false, + error: error.body?.message || error.body?.error || error.message, + }; + } + + if (error instanceof Error) { return { success: false, error: error.message, - } as T; + }; } return { success: false, error: 'Unknown error', - } as T; + }; } }; private getPath(path: Array): string { - return resolveUrl(`${WORKSPACES_API_BASE_URL}/`, join(...path)); + return [WORKSPACES_API_BASE_URL, join(...path)].filter((item) => item).join('/'); } private async updateWorkspaceListAndNotify(): Promise { @@ -128,7 +135,7 @@ export class WorkspacesClient { } public async getCurrentWorkspaceId(): Promise> { - const currentWorkspaceId = this.currentWorkspaceId; + const currentWorkspaceId = this.currentWorkspaceId$.getValue(); if (!currentWorkspaceId) { return { success: false, @@ -161,16 +168,9 @@ export class WorkspacesClient { public async create( attributes: Omit ): Promise> { - if (!attributes) { - return { - success: false, - error: 'Workspace attributes is required', - }; - } - const path = this.getPath([]); - const result = await this.catchedFetch>(path, { + const result = await this.safeFetch(path, { method: 'POST', body: JSON.stringify({ attributes, @@ -191,14 +191,7 @@ export class WorkspacesClient { * @returns */ public async delete(id: string): Promise> { - if (!id) { - return { - success: false, - error: 'Id is required.', - }; - } - - const result = await this.catchedFetch(this.getPath([id]), { method: 'DELETE' }); + const result = await this.safeFetch(this.getPath([id]), { method: 'DELETE' }); if (result.success) { this.updateWorkspaceListAndNotify(); @@ -230,7 +223,7 @@ export class WorkspacesClient { }> > => { const path = this.getPath(['_list']); - return this.catchedFetch(path, { + return this.safeFetch(path, { method: 'POST', body: JSON.stringify(options || {}), }); @@ -243,15 +236,8 @@ export class WorkspacesClient { * @returns The workspace for the given id. */ public async get(id: string): Promise> { - if (!id) { - return { - success: false, - error: 'Id is required.', - }; - } - const path = this.getPath([id]); - return this.catchedFetch(path, { + return this.safeFetch(path, { method: 'GET', }); } @@ -267,19 +253,12 @@ export class WorkspacesClient { id: string, attributes: Partial ): Promise> { - if (!id || !attributes) { - return { - success: false, - error: 'Id and attributes are required.', - }; - } - const path = this.getPath([id]); const body = { attributes, }; - const result = await this.catchedFetch(path, { + const result = await this.safeFetch(path, { method: 'PUT', body: JSON.stringify(body), }); diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts index 24345b6a34d9..980364103ba8 100644 --- a/src/core/server/workspaces/routes/index.ts +++ b/src/core/server/workspaces/routes/index.ts @@ -7,9 +7,7 @@ import { InternalHttpServiceSetup } from '../../http'; import { Logger } from '../../logging'; import { IWorkspaceDBImpl } from '../types'; -export const WORKSPACES_API_BASE_URL = '/api/workspaces'; - -export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; +const WORKSPACES_API_BASE_URL = '/api/workspaces'; export function registerRoutes({ client, @@ -71,7 +69,7 @@ export function registerRoutes({ ); router.post( { - path: '/', + path: '', validate: { body: schema.object({ attributes: schema.object({ @@ -98,7 +96,7 @@ export function registerRoutes({ ); router.put( { - path: '/{id}', + path: '/{id?}', validate: { params: schema.object({ id: schema.string(), @@ -130,7 +128,7 @@ export function registerRoutes({ ); router.delete( { - path: '/{id}', + path: '/{id?}', validate: { params: schema.object({ id: schema.string(), diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 911c9a651137..39069ca09bb8 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -4,7 +4,6 @@ */ import { i18n } from '@osd/i18n'; -import { parse } from 'querystring'; import { CoreSetup, CoreStart, @@ -28,9 +27,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } }); } - private getWorkpsaceIdFromQueryString(): string { - const querystringObject = parse(window.location.search.replace(/^\??/, '')); - return querystringObject[WORKSPACE_ID_QUERYSTRING_NAME] as string; + private getWorkpsaceIdFromQueryString(): string | null { + const searchParams = new URLSearchParams(window.location.search); + return searchParams.get(WORKSPACE_ID_QUERYSTRING_NAME); } private getWorkpsaceIdFromSessionStorage(): string { try { @@ -53,11 +52,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } public async setup(core: CoreSetup) { this.core = core; - /** - * register a listener - */ - this.addWorkspaceListener(); - /** * Retrive workspace id from url or sessionstorage * url > sessionstorage @@ -77,6 +71,12 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { ); } } + + /** + * register a listener + */ + this.addWorkspaceListener(); + core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', { From 89ec965a2e77b986e0507e5eb8ae0a63fd739d3d Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 15 Jun 2023 16:06:06 +0800 Subject: [PATCH 20/54] feat: integrate with workspace create API (#13) * feat: integrate with workspace create API Signed-off-by: Lin Wang * feat: update to i18n text for toast Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang --- .../workspace_creator/workspace_creator.tsx | 37 +++++++++++++++++-- .../workspace_creator/workspace_form.tsx | 2 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index e7006464ba7d..59c4ce0c444d 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -5,17 +5,48 @@ import React, { useCallback } from 'react'; import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; -import { WorkspaceForm } from './workspace_form'; +import { WorkspaceForm, WorkspaceFormData } from './workspace_form'; export const WorkspaceCreator = () => { const { - services: { application }, + services: { application, workspaces, notifications }, } = useOpenSearchDashboards(); - const handleWorkspaceFormSubmit = useCallback(() => {}, []); + const handleWorkspaceFormSubmit = useCallback( + async (data: WorkspaceFormData) => { + let result; + try { + result = await workspaces?.client.create(data); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.create.failed', { + defaultMessage: 'Failed to create workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.create.success', { + defaultMessage: 'Create workspace successfully', + }), + }); + return; + } + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.create.failed', { + defaultMessage: 'Failed to create workspace', + }), + text: result?.error, + }); + }, + [notifications?.toasts, workspaces?.client] + ); return ( diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx index 8cb3a1e3c39d..41639701c435 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -44,7 +44,7 @@ interface WorkspaceFeatureGroup { features: WorkspaceFeature[]; } -interface WorkspaceFormData { +export interface WorkspaceFormData { name: string; description?: string; features: string[]; From 28da56740e2671cdc44cb7441fdca08bc27a817e Mon Sep 17 00:00:00 2001 From: suzhou Date: Fri, 16 Jun 2023 09:16:49 +0800 Subject: [PATCH 21/54] Add currentWorkspace$ (#15) * feat: add currentWorkspace$ Signed-off-by: SuZhoue-Joe * fix: type error Signed-off-by: SuZhoue-Joe * feat: add emit on currentWorkspace$ Signed-off-by: SuZhoue-Joe --------- Signed-off-by: SuZhoue-Joe --- .../fatal_errors/fatal_errors_service.mock.ts | 6 ++- .../public/workspace/workspaces_client.ts | 46 ++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index 5079fc8f4b6a..e495d66ae568 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -30,7 +30,7 @@ import type { PublicMethodsOf } from '@osd/utility-types'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors_service'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { WorkspaceAttribute } from '../workspace'; const createSetupContractMock = () => { @@ -62,12 +62,14 @@ export const fatalErrorsServiceMock = { }; const currentWorkspaceId$ = new BehaviorSubject(''); -const workspaceList$ = new Subject(); +const workspaceList$ = new BehaviorSubject([]); +const currentWorkspace$ = new BehaviorSubject(null); const createWorkspacesSetupContractMock = () => ({ client: { currentWorkspaceId$, workspaceList$, + currentWorkspace$, stop: jest.fn(), enterWorkspace: jest.fn(), exitWorkspace: jest.fn(), diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 91d83dd1639f..f37fd89ae249 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ import type { PublicContract } from '@osd/utility-types'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { isEqual } from 'lodash'; import { HttpFetchError, HttpFetchOptions, HttpSetup } from '../http'; import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; import { WORKSPACES_API_BASE_URL, WORKSPACE_ERROR_REASON_MAP } from './consts'; @@ -41,26 +42,34 @@ export class WorkspacesClient { private http: HttpSetup; public currentWorkspaceId$ = new BehaviorSubject(''); public workspaceList$ = new BehaviorSubject([]); + public currentWorkspace$ = new BehaviorSubject(null); constructor(http: HttpSetup) { this.http = http; - /** - * Add logic to check if current workspace id is still valid - * If not, remove the current workspace id and notify other subscribers - */ - this.workspaceList$.subscribe(async (workspaceList) => { - const currentWorkspaceId = this.currentWorkspaceId$.getValue(); - if (currentWorkspaceId) { - const findItem = workspaceList.find((item) => item.id === currentWorkspaceId); - if (!findItem) { + + combineLatest([this.workspaceList$, this.currentWorkspaceId$]).subscribe( + ([workspaceList, currentWorkspaceId]) => { + const currentWorkspace = this.findWorkspace([workspaceList, currentWorkspaceId]); + + /** + * Do a simple idempotent verification here + */ + if (!isEqual(currentWorkspace, this.currentWorkspace$.getValue())) { + this.currentWorkspace$.next(currentWorkspace); + } + + if (currentWorkspaceId && !currentWorkspace?.id) { /** * Current workspace is staled */ this.currentWorkspaceId$.error({ reason: WORKSPACE_ERROR_REASON_MAP.WORKSPACE_STALED, }); + this.currentWorkspace$.error({ + reason: WORKSPACE_ERROR_REASON_MAP.WORKSPACE_STALED, + }); } } - }); + ); /** * Initialize workspace list @@ -68,6 +77,21 @@ export class WorkspacesClient { this.updateWorkspaceListAndNotify(); } + private findWorkspace(payload: [WorkspaceAttribute[], string]): WorkspaceAttribute | null { + const [workspaceList, currentWorkspaceId] = payload; + if (!currentWorkspaceId || !workspaceList || !workspaceList.length) { + return null; + } + + const findItem = workspaceList.find((item) => item?.id === currentWorkspaceId); + + if (!findItem) { + return null; + } + + return findItem; + } + /** * Add a non-throw-error fetch method for internal use. */ From edec3bdccd979bc5d7538fbb65c06af4d2fd628b Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 16 Jun 2023 09:36:37 +0800 Subject: [PATCH 22/54] register plugin with workspace template (#16) Signed-off-by: Hailong Cui --- src/plugins/dashboard/public/plugin.tsx | 7 ++++++- src/plugins/discover/public/plugin.ts | 5 +++++ src/plugins/visualize/public/plugin.ts | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 95c6e5d4bafd..b008c12c21c9 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -367,7 +367,12 @@ export class DashboardPlugin defaultPath: `#${DashboardConstants.LANDING_PAGE_PATH}`, updater$: this.appStateUpdater, category: DEFAULT_APP_CATEGORIES.opensearchDashboards, - workspaceTemplate: [DEFAULT_WORKSPACE_TEMPLATES.search], + workspaceTemplate: [ + DEFAULT_WORKSPACE_TEMPLATES.search, + DEFAULT_WORKSPACE_TEMPLATES.general_analysis, + DEFAULT_WORKSPACE_TEMPLATES.observability, + DEFAULT_WORKSPACE_TEMPLATES.security_analytics, + ], mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart, dashboardStart] = await core.getStartServices(); this.currentHistory = params.history; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 62f6e6908ba1..a4c5bb78d649 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -38,6 +38,7 @@ import { AppUpdater, CoreSetup, CoreStart, + DEFAULT_WORKSPACE_TEMPLATES, Plugin, PluginInitializerContext, } from 'opensearch-dashboards/public'; @@ -315,6 +316,10 @@ export class DiscoverPlugin euiIconType: 'inputOutput', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + workspaceTemplate: [ + DEFAULT_WORKSPACE_TEMPLATES.search, + DEFAULT_WORKSPACE_TEMPLATES.general_analysis, + ], mount: async (params: AppMountParameters) => { if (!this.initializeServices) { throw Error('Discover plugin method initializeServices is undefined'); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 297db26c48de..a5cbbfc849e3 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -37,6 +37,7 @@ import { AppUpdater, CoreSetup, CoreStart, + DEFAULT_WORKSPACE_TEMPLATES, Plugin, PluginInitializerContext, ScopedHistory, @@ -155,6 +156,10 @@ export class VisualizePlugin euiIconType: 'inputOutput', defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + workspaceTemplate: [ + DEFAULT_WORKSPACE_TEMPLATES.search, + DEFAULT_WORKSPACE_TEMPLATES.general_analysis, + ], updater$: this.appStateUpdater.asObservable(), // remove all references to visualize mount: async (params: AppMountParameters) => { From aba31ba869cb6e97a60857cdeae571fcc3578013 Mon Sep 17 00:00:00 2001 From: zhichao-aws Date: Fri, 16 Jun 2023 14:16:53 +0800 Subject: [PATCH 23/54] workspace dropdown list (#9) Add workspace dropdown list --------- Signed-off-by: zhichao-aws Signed-off-by: SuZhoue-Joe Signed-off-by: suzhou Co-authored-by: SuZhoue-Joe --- .../workspace_dropdown_list/index.ts | 8 ++ .../workspace_dropdown_list.tsx | 83 +++++++++++++++++++ src/plugins/workspace/public/mount.tsx | 32 +++++++ src/plugins/workspace/public/plugin.ts | 2 + 4 files changed, 125 insertions(+) create mode 100644 src/plugins/workspace/public/containers/workspace_dropdown_list/index.ts create mode 100644 src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx create mode 100644 src/plugins/workspace/public/mount.tsx diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/index.ts b/src/plugins/workspace/public/containers/workspace_dropdown_list/index.ts new file mode 100644 index 000000000000..b68e1f9131e1 --- /dev/null +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WorkspaceDropdownList } from './workspace_dropdown_list'; + +export { WorkspaceDropdownList }; diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx new file mode 100644 index 000000000000..276a5872473e --- /dev/null +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useCallback, useMemo, useEffect } from 'react'; + +import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; + +type WorkspaceOption = EuiComboBoxOptionOption; + +interface WorkspaceDropdownListProps { + coreStart: CoreStart; + onCreateWorkspace: () => void; + onSwitchWorkspace: (workspaceId: string) => Promise; +} + +function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { + return { label: workspace.name, key: workspace.id, value: workspace }; +} + +export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { + const { coreStart, onCreateWorkspace, onSwitchWorkspace } = props; + const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); + const currentWorkspaceId = useObservable(coreStart.workspaces.client.currentWorkspaceId$, ''); + + const [loading, setLoading] = useState(false); + const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); + + const currentWorkspaceOption = useMemo(() => { + const workspace = workspaceList.find((item) => item.id === currentWorkspaceId); + if (!workspace) { + coreStart.notifications.toasts.addDanger( + `can not get current workspace of id [${currentWorkspaceId}]` + ); + return [workspaceToOption({ id: currentWorkspaceId, name: '' })]; + } + return [workspaceToOption(workspace)]; + }, [workspaceList, currentWorkspaceId, coreStart]); + const allWorkspaceOptions = useMemo(() => { + return workspaceList.map(workspaceToOption); + }, [workspaceList]); + + const onSearchChange = useCallback( + (searchValue: string) => { + setWorkspaceOptions(allWorkspaceOptions.filter((item) => item.label.includes(searchValue))); + }, + [allWorkspaceOptions] + ); + + const onChange = (workspaceOption: WorkspaceOption[]) => { + /** switch the workspace */ + setLoading(true); + onSwitchWorkspace(workspaceOption[0].key!) + .catch((err) => + coreStart.notifications.toasts.addDanger('some error happens in workspace service') + ) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + onSearchChange(''); + }, [onSearchChange]); + + return ( + <> + Create workspace} + /> + + ); +} diff --git a/src/plugins/workspace/public/mount.tsx b/src/plugins/workspace/public/mount.tsx new file mode 100644 index 000000000000..17646ebecd28 --- /dev/null +++ b/src/plugins/workspace/public/mount.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { CoreStart } from '../../../core/public'; +import { WorkspaceDropdownList } from './containers/workspace_dropdown_list'; + +export const mountDropdownList = (core: CoreStart) => { + core.chrome.navControls.registerLeft({ + order: 0, + mount: (element) => { + ReactDOM.render( + alert('create')} + onSwitchWorkspace={async (id: string) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + alert(`switch to workspace ${id}`); + }} + // onSwitchWorkspace={(id: string) => alert(`switch to workspace ${id}`)} + />, + element + ); + return () => { + ReactDOM.unmountComponentAtNode(element); + }; + }, + }); +}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 39069ca09bb8..5a70235373b7 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -13,6 +13,7 @@ import { } from '../../../core/public'; import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../common/constants'; import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; +import { mountDropdownList } from './mount'; export class WorkspacesPlugin implements Plugin<{}, {}> { private core?: CoreSetup; @@ -100,6 +101,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } public start(core: CoreStart) { + mountDropdownList(core); return {}; } } From 1132fc7f5460217e7724164a7fe3bc6caa1842c9 Mon Sep 17 00:00:00 2001 From: raintygao Date: Fri, 16 Jun 2023 14:17:10 +0800 Subject: [PATCH 24/54] init workspace menu stage 1 (#12) * feat: init workspace menu stage 1 Signed-off-by: tygao * fix: remove port diff Signed-off-by: tygao * feat: update menu logic Signed-off-by: tygao --------- Signed-off-by: tygao --- src/core/public/chrome/chrome_service.tsx | 5 +++- .../chrome/ui/header/collapsible_nav.tsx | 27 +++++++++++++++++-- src/core/public/chrome/ui/header/header.tsx | 5 +++- src/core/public/core_system.ts | 1 + 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 97746f465abc..6b33b77a6f74 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -48,7 +48,7 @@ import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { ChromeHelpExtensionMenuLink } from './ui/header/header_help_menu'; -import { Branding } from '../'; +import { Branding, WorkspacesStart } from '../'; export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; @@ -99,6 +99,7 @@ interface StartDeps { injectedMetadata: InjectedMetadataStart; notifications: NotificationsStart; uiSettings: IUiSettingsClient; + workspaces: WorkspacesStart; } /** @internal */ @@ -152,6 +153,7 @@ export class ChromeService { injectedMetadata, notifications, uiSettings, + workspaces, }: StartDeps): Promise { this.initVisibility(application); @@ -262,6 +264,7 @@ export class ChromeService { isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} survey={injectedMetadata.getSurvey()} + currentWorkspace$={workspaces.client.currentWorkspace$} /> ), diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 51d43d96f7fd..549f13997f79 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -51,6 +51,7 @@ import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; import { ChromeBranding } from '../../chrome_service'; +import { WorkspaceAttribute } from '../../../workspace'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -102,6 +103,7 @@ interface Props { navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; branding: ChromeBranding; + currentWorkspace$: Rx.BehaviorSubject; } export function CollapsibleNav({ @@ -122,11 +124,14 @@ export function CollapsibleNav({ const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); const customNavLink = useObservable(observables.customNavLink$, undefined); const appId = useObservable(observables.appId$, ''); + const currentWorkspace = useObservable(observables.currentWorkspace$); const lockRef = useRef(null); const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; - const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); + const filterdLinks = getFilterLinks(currentWorkspace, allCategorizedLinks); + const categoryDictionary = getAllCategories(filterdLinks); + const orderedCategories = getOrderedCategories(filterdLinks, categoryDictionary); + const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { return createEuiListItem({ link, @@ -145,6 +150,24 @@ export function CollapsibleNav({ const markDefault = branding.mark?.defaultUrl; const markDarkMode = branding.mark?.darkModeUrl; + function getFilterLinks( + workspace: WorkspaceAttribute | null | undefined, + categorizedLinks: Record + ) { + // plugins are in this dictionary + const pluginsDictionary = categorizedLinks.opensearch; + if (!pluginsDictionary) return categorizedLinks; + + const features = workspace?.features ?? []; + const newPluginsDictionary = pluginsDictionary.filter((item) => features.indexOf(item.id) > -1); + if (newPluginsDictionary.length === 0) { + delete categorizedLinks.opensearch; + } else { + categorizedLinks.opensearch = newPluginsDictionary; + } + return categorizedLinks; + } + /** * Use branding configurations to check which URL to use for rendering * side menu opensearch logo in default mode diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index a78371f4f264..a2b218ae4087 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -42,7 +42,7 @@ import { i18n } from '@osd/i18n'; import classnames from 'classnames'; import React, { createRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { Observable } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { LoadingIndicator } from '../'; import { ChromeBadge, @@ -63,6 +63,7 @@ import { HomeLoader } from './home_loader'; import { HeaderNavControls } from './header_nav_controls'; import { HeaderActionMenu } from './header_action_menu'; import { HeaderLogo } from './header_logo'; +import { WorkspaceAttribute } from '../../../workspace'; export interface HeaderProps { opensearchDashboardsVersion: string; @@ -90,6 +91,7 @@ export interface HeaderProps { onIsLockedUpdate: OnIsLockedUpdate; branding: ChromeBranding; survey: string | undefined; + currentWorkspace$: BehaviorSubject; } export function Header({ @@ -255,6 +257,7 @@ export function Header({ }} customNavLink$={observables.customNavLink$} branding={branding} + currentWorkspace$={observables.currentWorkspace$} /> diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 1c0286cbd03c..9512560112f7 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -233,6 +233,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }); this.coreApp.start({ application, http, notifications, uiSettings }); From 057b808e9d680316c623178aa564ce530deccc83 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 16 Jun 2023 17:35:17 +0800 Subject: [PATCH 25/54] Fix template registration import error (#21) * fix import error Signed-off-by: Hailong Cui * fix osd bootstrap failure Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- src/core/public/chrome/chrome_service.test.ts | 2 ++ src/core/public/chrome/ui/header/collapsible_nav.test.tsx | 2 ++ src/core/public/chrome/ui/header/header.test.tsx | 2 ++ src/plugins/discover/public/plugin.ts | 3 +-- src/plugins/visualize/public/plugin.ts | 4 +--- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index b8635f5a070f..6e36775b90c2 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 { workspacesServiceMock } from '../fatal_errors/fatal_errors_service.mock'; class FakeApp implements App { public title = `${this.id} App`; @@ -67,6 +68,7 @@ function defaultStartDeps(availableApps?: App[]) { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; if (availableApps) { diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index dc44fe5053fe..4df3f68ec90e 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -37,6 +37,7 @@ import { ChromeNavLink, DEFAULT_APP_CATEGORIES } from '../../..'; import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; +import { workspacesServiceMock } from '../../../fatal_errors/fatal_errors_service.mock'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -80,6 +81,7 @@ function mockProps() { navigateToApp: () => Promise.resolve(), navigateToUrl: () => Promise.resolve(), customNavLink$: new BehaviorSubject(undefined), + currentWorkspace$: workspacesServiceMock.createStartContract().client.currentWorkspace$, branding: { darkMode: false, mark: { diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 319dea4c394b..cd969fcc7275 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -36,6 +36,7 @@ import { httpServiceMock } from '../../../http/http_service.mock'; import { applicationServiceMock } from '../../../mocks'; import { Header } from './header'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { workspacesServiceMock } from '../../../fatal_errors/fatal_errors_service.mock'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -76,6 +77,7 @@ function mockProps() { applicationTitle: 'OpenSearch Dashboards', }, survey: '/', + currentWorkspace$: workspacesServiceMock.createStartContract().client.currentWorkspace$, }; } diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index a4c5bb78d649..21f80ab61b75 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -38,7 +38,6 @@ import { AppUpdater, CoreSetup, CoreStart, - DEFAULT_WORKSPACE_TEMPLATES, Plugin, PluginInitializerContext, } from 'opensearch-dashboards/public'; @@ -60,7 +59,7 @@ import rison from 'rison-node'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { DEFAULT_APP_CATEGORIES, DEFAULT_WORKSPACE_TEMPLATES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewLink } from './application/doc_views_links/doc_views_links_types'; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index a5cbbfc849e3..5fb5d523edb2 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -37,12 +37,10 @@ import { AppUpdater, CoreSetup, CoreStart, - DEFAULT_WORKSPACE_TEMPLATES, Plugin, PluginInitializerContext, ScopedHistory, } from 'opensearch-dashboards/public'; - import { Storage, createOsdUrlTracker, @@ -57,7 +55,7 @@ import { VisualizationsStart } from '../../visualizations/public'; import { VisualizeConstants } from './application/visualize_constants'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { VisualizeServices } from './application/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { DEFAULT_APP_CATEGORIES, DEFAULT_WORKSPACE_TEMPLATES } from '../../../core/public'; import { SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; import { DashboardStart } from '../../dashboard/public'; From 60471085c274cc434834971e0f9c4c4065633b9d Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 19 Jun 2023 13:45:14 +0800 Subject: [PATCH 26/54] Add workspace overview page (#19) * feat: add workspace overview page Signed-off-by: Lin Wang * refactor: move paths to common constants Signed-off-by: Lin Wang * feat: add workspace overview item by custom nav in start phase Signed-off-by: Lin Wang * refactor: change to currentWorkspace$ in workspace client Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 5 +++ .../workspace/public/components/routes.ts | 14 ++++--- .../public/components/workspace_overview.tsx | 40 +++++++++++++++++++ src/plugins/workspace/public/plugin.ts | 8 +++- 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_overview.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 4ac1575c25f7..633c402ffa86 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -6,3 +6,8 @@ export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; + +export const PATHS = { + create: '/create', + overview: '/overview', +}; diff --git a/src/plugins/workspace/public/components/routes.ts b/src/plugins/workspace/public/components/routes.ts index 5e47465643f5..99a0a1402afa 100644 --- a/src/plugins/workspace/public/components/routes.ts +++ b/src/plugins/workspace/public/components/routes.ts @@ -3,11 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { WorkspaceCreator } from './workspace_creator'; +import { PATHS } from '../../common/constants'; -export const paths = { - create: '/create', -}; +import { WorkspaceCreator } from './workspace_creator'; +import { WorkspaceOverview } from './workspace_overview'; export interface RouteConfig { path: string; @@ -18,8 +17,13 @@ export interface RouteConfig { export const ROUTES: RouteConfig[] = [ { - path: paths.create, + path: PATHS.create, Component: WorkspaceCreator, label: 'Create', }, + { + path: PATHS.overview, + Component: WorkspaceOverview, + label: 'Overview', + }, ]; diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx new file mode 100644 index 000000000000..0da5b7cd1e66 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useObservable } from 'react-use'; +import { of } from 'rxjs'; + +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; + +export const WorkspaceOverview = () => { + const { + services: { workspaces }, + } = useOpenSearchDashboards(); + + const currentWorkspace = useObservable( + workspaces ? workspaces.client.currentWorkspace$ : of(null) + ); + + return ( + <> + Delete, + Update, + ]} + /> + + +

Workspace

+
+ + {JSON.stringify(currentWorkspace)} +
+ + ); +}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 5a70235373b7..df601a3f1a29 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -11,7 +11,7 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../common/constants'; +import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE, PATHS } from '../common/constants'; import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; import { mountDropdownList } from './mount'; @@ -102,6 +102,12 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { public start(core: CoreStart) { mountDropdownList(core); + + core.chrome.setCustomNavLink({ + title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), + baseUrl: core.http.basePath.get(), + href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.overview }), + }); return {}; } } From 724ba554047f679bc10297d9b6ade00b5cf0aa3b Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 19 Jun 2023 16:55:39 +0800 Subject: [PATCH 27/54] feat: navigate to workspace create page after button clicked (#23) Signed-off-by: Lin Wang --- .../workspace_dropdown_list.tsx | 10 +++++++--- src/plugins/workspace/public/mount.tsx | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index 276a5872473e..4285f7b2fa49 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -8,12 +8,12 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; +import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; type WorkspaceOption = EuiComboBoxOptionOption; interface WorkspaceDropdownListProps { coreStart: CoreStart; - onCreateWorkspace: () => void; onSwitchWorkspace: (workspaceId: string) => Promise; } @@ -22,7 +22,7 @@ function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { } export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { - const { coreStart, onCreateWorkspace, onSwitchWorkspace } = props; + const { coreStart, onSwitchWorkspace } = props; const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); const currentWorkspaceId = useObservable(coreStart.workspaces.client.currentWorkspaceId$, ''); @@ -62,6 +62,10 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { }); }; + const onCreateWorkspaceClick = () => { + coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); + }; + useEffect(() => { onSearchChange(''); }, [onSearchChange]); @@ -76,7 +80,7 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { selectedOptions={currentWorkspaceOption} singleSelection={{ asPlainText: true }} onSearchChange={onSearchChange} - append={Create workspace} + append={Create workspace} /> ); diff --git a/src/plugins/workspace/public/mount.tsx b/src/plugins/workspace/public/mount.tsx index 17646ebecd28..03e4cb38a3a9 100644 --- a/src/plugins/workspace/public/mount.tsx +++ b/src/plugins/workspace/public/mount.tsx @@ -15,7 +15,6 @@ export const mountDropdownList = (core: CoreStart) => { ReactDOM.render( alert('create')} onSwitchWorkspace={async (id: string) => { await new Promise((resolve) => setTimeout(resolve, 1000)); alert(`switch to workspace ${id}`); From 5fbbd095c7b0e11be12d1aea5840bc42ad140e9f Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Mon, 19 Jun 2023 21:08:35 +0800 Subject: [PATCH 28/54] fix failed test snapshots (#22) fix failed test snapshots temporary fix: fetch functional test from main branch fixed git error which cannot find ref due to feature branch `workspace` not exists on repo opensearch-dashboards-functional-test Signed-off-by: Yulong Ruan --------- Signed-off-by: Yulong Ruan --- .github/workflows/cypress_workflow.yml | 7 +- .../collapsible_nav.test.tsx.snap | 1497 ++++++++++++++++- .../header/__snapshots__/header.test.tsx.snap | 270 +++ 3 files changed, 1687 insertions(+), 87 deletions(-) diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 5e78785f9b88..edfcc288fad7 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -55,7 +55,8 @@ jobs: with: path: ${{ env.FTR_PATH }} repository: opensearch-project/opensearch-dashboards-functional-test - ref: '${{ github.base_ref }}' + # revert this to '${{ github.base_ref }}' + ref: 'main' - name: Get Cypress version id: cypress_version @@ -88,7 +89,7 @@ jobs: name: ftr-cypress-screenshots path: ${{ env.FTR_PATH }}/cypress/screenshots retention-days: 1 - + - uses: actions/upload-artifact@v3 if: always() with: @@ -101,4 +102,4 @@ jobs: with: name: ftr-cypress-results path: ${{ env.FTR_PATH }}/cypress/results - retention-days: 1 \ No newline at end of file + retention-days: 1 diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 9bdbec781c5e..382f88a562ce 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -70,6 +70,92 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -1964,6 +2050,55 @@ exports[`CollapsibleNav renders the default nav 1`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -2209,6 +2344,55 @@ exports[`CollapsibleNav renders the default nav 2`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -2455,6 +2639,55 @@ exports[`CollapsibleNav renders the default nav 3`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -2992,10 +3225,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = } } closeNav={[Function]} - customNavLink$={ + currentWorkspace$={ BehaviorSubject { "_isScalar": false, - "_value": undefined, + "_value": null, "closed": false, "hasError": false, "isStopped": false, @@ -3037,35 +3270,269 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - Object { - "baseUrl": "/", - "category": Object { + 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, + }, + 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, + }, + 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, + } + } + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "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, + } + } + homeHref="/" + id="collapsibe-nav" + isLocked={false} + isNavOpen={true} + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "/", + "category": Object { + "euiIconType": "inputOutput", + "id": "opensearchDashboards", + "label": "OpenSearch Dashboards", + "order": 1000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + Object { + "baseUrl": "/", + "category": Object { "euiIconType": "logoObservability", "id": "observability", "label": "Observability", @@ -4019,24 +4486,258 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = "thrownError": null, } } - basePath={ - BasePath { - "basePath": "/test", - "get": [Function], - "prepend": [Function], - "remove": [Function], - "serverBasePath": "/test", - } - } - branding={ - Object { - "darkMode": true, - "mark": Object { - "defaultUrl": "/defaultModeLogo", - }, - } - } - closeNav={[Function]} + basePath={ + BasePath { + "basePath": "/test", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + } + } + branding={ + Object { + "darkMode": true, + "mark": Object { + "defaultUrl": "/defaultModeLogo", + }, + } + } + closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + }, + 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, + }, + 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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -5064,22 +5765,256 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "thrownError": null, } } - basePath={ - BasePath { - "basePath": "/test", - "get": [Function], - "prepend": [Function], - "remove": [Function], - "serverBasePath": "/test", - } - } - branding={ - Object { - "darkMode": false, - "mark": Object {}, - } - } - closeNav={[Function]} + basePath={ + BasePath { + "basePath": "/test", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + } + } + branding={ + Object { + "darkMode": false, + "mark": Object {}, + } + } + closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + }, + 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, + }, + 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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -6107,25 +7042,222 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "thrownError": null, } } - basePath={ - BasePath { - "basePath": "/test", - "get": [Function], - "prepend": [Function], - "remove": [Function], - "serverBasePath": "/test", - } - } - branding={ - Object { - "darkMode": false, - "mark": Object { - "darkModeUrl": "/darkModeLogo", - "defaultUrl": "/defaultModeLogo", - }, - } - } - closeNav={[Function]} + basePath={ + BasePath { + "basePath": "/test", + "get": [Function], + "prepend": [Function], + "remove": [Function], + "serverBasePath": "/test", + } + } + branding={ + Object { + "darkMode": false, + "mark": Object { + "darkModeUrl": "/darkModeLogo", + "defaultUrl": "/defaultModeLogo", + }, + } + } + closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + }, + 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, + }, + 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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -7169,6 +8301,203 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + }, + 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, + }, + 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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, 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 4c2beb329a98..5ee36fc58662 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 @@ -312,6 +312,55 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -5614,6 +5663,55 @@ exports[`Header handles visibility and lock changes 1`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -6670,6 +6768,92 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, @@ -10858,6 +11042,92 @@ exports[`Header renders condensed header 1`] = ` } } closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + } + } customNavLink$={ BehaviorSubject { "_isScalar": false, From 60b42782eec46f6a9f18c78024ca4dcab6d3d290 Mon Sep 17 00:00:00 2001 From: zhichao-aws Date: Tue, 20 Jun 2023 12:11:39 +0800 Subject: [PATCH 29/54] change to currentWorkspace, wrap title using i18n (#20) * change to currentWorkspace, wrap title using i18n Signed-off-by: zhichao-aws * change import Signed-off-by: zhichao-aws * directly return [] if currentWorkspace is null Signed-off-by: zhichao-aws --------- Signed-off-by: zhichao-aws --- .../workspace_dropdown_list.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index 4285f7b2fa49..cb411daee599 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@osd/i18n'; import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; @@ -21,24 +22,26 @@ function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { return { label: workspace.name, key: workspace.id, value: workspace }; } +export function getErrorMessage(err: any) { + if (err && err.message) return err.message; + return ''; +} + export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { const { coreStart, onSwitchWorkspace } = props; const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); - const currentWorkspaceId = useObservable(coreStart.workspaces.client.currentWorkspaceId$, ''); + const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); const [loading, setLoading] = useState(false); const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); const currentWorkspaceOption = useMemo(() => { - const workspace = workspaceList.find((item) => item.id === currentWorkspaceId); - if (!workspace) { - coreStart.notifications.toasts.addDanger( - `can not get current workspace of id [${currentWorkspaceId}]` - ); - return [workspaceToOption({ id: currentWorkspaceId, name: '' })]; + if (!currentWorkspace) { + return []; + } else { + return [workspaceToOption(currentWorkspace)]; } - return [workspaceToOption(workspace)]; - }, [workspaceList, currentWorkspaceId, coreStart]); + }, [currentWorkspace]); const allWorkspaceOptions = useMemo(() => { return workspaceList.map(workspaceToOption); }, [workspaceList]); @@ -55,7 +58,12 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { setLoading(true); onSwitchWorkspace(workspaceOption[0].key!) .catch((err) => - coreStart.notifications.toasts.addDanger('some error happens in workspace service') + coreStart.notifications.toasts.addDanger({ + title: i18n.translate('workspace.dropdownList.switchWorkspaceErrorTitle', { + defaultMessage: 'some error happens when switching workspace', + }), + text: getErrorMessage(err), + }) ) .finally(() => { setLoading(false); From be11d98ffa6e623a154a652a4b3e5697e41227f2 Mon Sep 17 00:00:00 2001 From: raintygao Date: Tue, 20 Jun 2023 15:24:19 +0800 Subject: [PATCH 30/54] add workspace switch (#17) * feat: update workspace switch Signed-off-by: tygao * fix: fix switch error Signed-off-by: tygao * fix: fix prettier after merge Signed-off-by: tygao * chore: remove extra code after merge Signed-off-by: tygao --------- Signed-off-by: tygao --- .../workspace_dropdown_list.tsx | 34 ++++++++----------- src/plugins/workspace/public/mount.tsx | 12 +------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index cb411daee599..f7e0113e2c4a 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -7,7 +7,6 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; -import { i18n } from '@osd/i18n'; import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; @@ -15,7 +14,6 @@ type WorkspaceOption = EuiComboBoxOptionOption; interface WorkspaceDropdownListProps { coreStart: CoreStart; - onSwitchWorkspace: (workspaceId: string) => Promise; } function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { @@ -28,7 +26,8 @@ export function getErrorMessage(err: any) { } export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { - const { coreStart, onSwitchWorkspace } = props; + const { coreStart } = props; + const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); @@ -53,22 +52,19 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { [allWorkspaceOptions] ); - const onChange = (workspaceOption: WorkspaceOption[]) => { - /** switch the workspace */ - setLoading(true); - onSwitchWorkspace(workspaceOption[0].key!) - .catch((err) => - coreStart.notifications.toasts.addDanger({ - title: i18n.translate('workspace.dropdownList.switchWorkspaceErrorTitle', { - defaultMessage: 'some error happens when switching workspace', - }), - text: getErrorMessage(err), - }) - ) - .finally(() => { - setLoading(false); - }); - }; + const onChange = useCallback( + (workspaceOption: WorkspaceOption[]) => { + /** switch the workspace */ + setLoading(true); + const id = workspaceOption[0].key!; + const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId(window.location.href, id); + if (newUrl) { + window.location.href = newUrl; + } + setLoading(false); + }, + [coreStart.workspaces] + ); const onCreateWorkspaceClick = () => { coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); diff --git a/src/plugins/workspace/public/mount.tsx b/src/plugins/workspace/public/mount.tsx index 03e4cb38a3a9..c4ca29479d23 100644 --- a/src/plugins/workspace/public/mount.tsx +++ b/src/plugins/workspace/public/mount.tsx @@ -12,17 +12,7 @@ export const mountDropdownList = (core: CoreStart) => { core.chrome.navControls.registerLeft({ order: 0, mount: (element) => { - ReactDOM.render( - { - await new Promise((resolve) => setTimeout(resolve, 1000)); - alert(`switch to workspace ${id}`); - }} - // onSwitchWorkspace={(id: string) => alert(`switch to workspace ${id}`)} - />, - element - ); + ReactDOM.render(, element); return () => { ReactDOM.unmountComponentAtNode(element); }; From 72203210745dccbd62a96b7544cfbf35f54172ff Mon Sep 17 00:00:00 2001 From: gaobinlong Date: Wed, 21 Jun 2023 10:46:57 +0800 Subject: [PATCH 31/54] Add update workspace page (#25) Signed-off-by: gaobinlong --- src/plugins/workspace/common/constants.ts | 3 + .../workspace/public/components/routes.ts | 6 + .../workspace_creator/workspace_creator.tsx | 7 +- .../workspace_creator/workspace_form.tsx | 26 +++- .../public/components/workspace_overview.tsx | 24 +++- .../components/workspace_updater/index.tsx | 6 + .../workspace_updater/workspace_updater.tsx | 117 ++++++++++++++++++ 7 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_updater/index.tsx create mode 100644 src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 633c402ffa86..903f028539dd 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -10,4 +10,7 @@ export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; export const PATHS = { create: '/create', overview: '/overview', + update: '/update', }; +export const WORKSPACE_OP_TYPE_CREATE = 'create'; +export const WORKSPACE_OP_TYPE_UPDATE = 'update'; diff --git a/src/plugins/workspace/public/components/routes.ts b/src/plugins/workspace/public/components/routes.ts index 99a0a1402afa..33f7b4774713 100644 --- a/src/plugins/workspace/public/components/routes.ts +++ b/src/plugins/workspace/public/components/routes.ts @@ -6,6 +6,7 @@ import { PATHS } from '../../common/constants'; import { WorkspaceCreator } from './workspace_creator'; +import { WorkspaceUpdater } from './workspace_updater'; import { WorkspaceOverview } from './workspace_overview'; export interface RouteConfig { @@ -26,4 +27,9 @@ export const ROUTES: RouteConfig[] = [ Component: WorkspaceOverview, label: 'Overview', }, + { + path: PATHS.update, + Component: WorkspaceUpdater, + label: 'Update', + }, ]; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index 59c4ce0c444d..e4b5a4cf7693 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -10,6 +10,7 @@ import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; import { WorkspaceForm, WorkspaceFormData } from './workspace_form'; +import { WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants'; export const WorkspaceCreator = () => { const { @@ -61,7 +62,11 @@ export const WorkspaceCreator = () => { style={{ width: '100%', maxWidth: 1000 }} > {application && ( - + )} diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx index 41639701c435..e561c3850827 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -32,6 +32,7 @@ import { import { WorkspaceTemplate } from '../../../../../core/types'; import { AppNavLinkStatus, ApplicationStart } from '../../../../../core/public'; import { useApplications, useWorkspaceTemplate } from '../../hooks'; +import { WORKSPACE_OP_TYPE_CREATE, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; interface WorkspaceFeature { id: string; @@ -62,8 +63,14 @@ interface WorkspaceFormProps { application: ApplicationStart; onSubmit?: (formData: WorkspaceFormData) => void; defaultValues?: WorkspaceFormData; + opType?: string; } -export const WorkspaceForm = ({ application, onSubmit, defaultValues }: WorkspaceFormProps) => { +export const WorkspaceForm = ({ + application, + onSubmit, + defaultValues, + opType, +}: WorkspaceFormProps) => { const { workspaceTemplates, templateFeatureMap } = useWorkspaceTemplate(application); const applications = useApplications(application); @@ -199,7 +206,7 @@ export const WorkspaceForm = ({ application, onSubmit, defaultValues }: Workspac - + } > - + @@ -329,9 +336,16 @@ export const WorkspaceForm = ({ application, onSubmit, defaultValues }: Workspac - - Create workspace - + {opType === WORKSPACE_OP_TYPE_CREATE && ( + + Create workspace + + )} + {opType === WORKSPACE_OP_TYPE_UPDATE && ( + + Update workspace + + )} ); diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index 0da5b7cd1e66..76986f141784 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -7,25 +7,43 @@ import React from 'react'; import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; +import { i18n } from '@osd/i18n'; +import { PATHS } from '../../common/constants'; +import { ApplicationStart } from '../../../../core/public'; +import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../../common/constants'; import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; export const WorkspaceOverview = () => { const { - services: { workspaces }, - } = useOpenSearchDashboards(); + services: { workspaces, application, notifications }, + } = useOpenSearchDashboards<{ application: ApplicationStart }>(); const currentWorkspace = useObservable( workspaces ? workspaces.client.currentWorkspace$ : of(null) ); + const onUpdateWorkspaceClick = () => { + if (!currentWorkspace || !currentWorkspace.id) { + notifications?.toasts.addDanger({ + title: i18n.translate('Cannot find current workspace', { + defaultMessage: 'Cannot update workspace', + }), + }); + return; + } + application.navigateToApp(WORKSPACE_APP_ID, { + path: PATHS.update + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, + }); + }; + return ( <> Delete, - Update, + Update, ]} /> diff --git a/src/plugins/workspace/public/components/workspace_updater/index.tsx b/src/plugins/workspace/public/components/workspace_updater/index.tsx new file mode 100644 index 000000000000..711f19fd25f6 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_updater/index.tsx @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WorkspaceUpdater } from './workspace_updater'; diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx new file mode 100644 index 000000000000..2706ee5363d5 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { useObservable } from 'react-use'; +import { i18n } from '@osd/i18n'; +import { of } from 'rxjs'; + +import { WorkspaceAttribute } from 'opensearch-dashboards/public'; +import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; + +import { PATHS } from '../../../common/constants'; +import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; +import { + WORKSPACE_APP_ID, + WORKSPACE_ID_IN_SESSION_STORAGE, + WORKSPACE_OP_TYPE_UPDATE, +} from '../../../common/constants'; +import { ApplicationStart } from '../../../../../core/public'; + +export const WorkspaceUpdater = () => { + const { + services: { application, workspaces, notifications }, + } = useOpenSearchDashboards<{ application: ApplicationStart }>(); + + const currentWorkspace = useObservable( + workspaces ? workspaces.client.currentWorkspace$ : of(null) + ); + + const excludedAttribute = 'id'; + const { [excludedAttribute]: removedProperty, ...otherAttributes } = + currentWorkspace || ({} as WorkspaceAttribute); + + const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState< + Omit + >(otherAttributes); + + useEffect(() => { + const { id, ...others } = currentWorkspace || ({} as WorkspaceAttribute); + setCurrentWorkspaceFormData(others); + }, [workspaces, currentWorkspace, excludedAttribute]); + + const handleWorkspaceFormSubmit = useCallback( + async (data: WorkspaceFormData) => { + let result; + if (!currentWorkspace) { + notifications?.toasts.addDanger({ + title: i18n.translate('Cannot find current workspace', { + defaultMessage: 'Cannot update workspace', + }), + }); + return; + } + try { + result = await workspaces?.client.update(currentWorkspace?.id, data); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.update.failed', { + defaultMessage: 'Failed to update workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.update.success', { + defaultMessage: 'Update workspace successfully', + }), + }); + application.navigateToApp(WORKSPACE_APP_ID, { + path: PATHS.overview + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, + }); + return; + } + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.update.failed', { + defaultMessage: 'Failed to update workspace', + }), + text: result?.error, + }); + }, + [notifications?.toasts, workspaces?.client, currentWorkspace, application] + ); + + if (!currentWorkspaceFormData.name) { + return null; + } + + return ( + + + + + {application && ( + + )} + + + + ); +}; From 4c3e1e0b214f7c274c2b9b4bea81315847c8a6dc Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Wed, 21 Jun 2023 14:59:55 +0800 Subject: [PATCH 32/54] Delete Workspace (#24) * add delete workspace modal Signed-off-by: yuye-aws * implement delete on workspace overview page Signed-off-by: yuye-aws * fix export on delete workspace modal Signed-off-by: yuye-aws * add try catch to handle errors for workspace delete Signed-off-by: yuye-aws * move visibility control to workspace overview page exlusively Signed-off-by: yuye-aws * remove unused import Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws --- .../delete_workspace_modal.tsx | 71 +++++++++++++++++++ .../delete_workspace_modal/index.ts | 6 ++ .../public/components/workspace_overview.tsx | 56 +++++++++++++-- 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx create mode 100644 src/plugins/workspace/public/components/delete_workspace_modal/index.ts diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx new file mode 100644 index 000000000000..d1f29a140718 --- /dev/null +++ b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +interface DeleteWorkspaceModalProps { + selectedItems: string[]; + onClose: () => void; + onConfirm: () => void; +} + +export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { + const [value, setValue] = useState(''); + const { onClose, onConfirm, selectedItems } = props; + + return ( + + + Delete workspace + + + +
+

The following workspace will be permanently deleted. This action cannot be undone.

+
    + {selectedItems.map((item) => ( +
  • {item}
  • + ))} +
+ + + To confirm your action, type delete. + + setValue(e.target.value)} + /> +
+
+ + + Cancel + + Delete + + +
+ ); +} diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/index.ts b/src/plugins/workspace/public/components/delete_workspace_modal/index.ts new file mode 100644 index 000000000000..3466e180c54a --- /dev/null +++ b/src/plugins/workspace/public/components/delete_workspace_modal/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './delete_workspace_modal'; diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index 76986f141784..b0eabce71555 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; import { i18n } from '@osd/i18n'; -import { PATHS } from '../../common/constants'; import { ApplicationStart } from '../../../../core/public'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { DeleteWorkspaceModal } from './delete_workspace_modal'; +import { PATHS } from '../../common/constants'; import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../../common/constants'; -import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; - export const WorkspaceOverview = () => { const { services: { workspaces, application, notifications }, @@ -23,6 +23,43 @@ export const WorkspaceOverview = () => { workspaces ? workspaces.client.currentWorkspace$ : of(null) ); + const workspaceId = currentWorkspace?.id; + const workspaceName = currentWorkspace?.name; + const [deleteWorkspaceModalVisible, setDeleteWorkspaceModalVisible] = useState(false); + + const deleteWorkspace = async () => { + if (workspaceId) { + let result; + try { + result = await workspaces?.client.delete(workspaceId); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return setDeleteWorkspaceModalVisible(false); + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.delete.success', { + defaultMessage: 'Delete workspace successfully', + }), + }); + } else { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: result?.error, + }); + } + } + setDeleteWorkspaceModalVisible(false); + await application.navigateToApp('home'); + }; + const onUpdateWorkspaceClick = () => { if (!currentWorkspace || !currentWorkspace.id) { notifications?.toasts.addDanger({ @@ -42,11 +79,22 @@ export const WorkspaceOverview = () => { setDeleteWorkspaceModalVisible(true)}> + Delete + , + Update, Delete, Update, ]} /> + {deleteWorkspaceModalVisible && ( + setDeleteWorkspaceModalVisible(false)} + selectedItems={workspaceName ? [workspaceName] : []} + /> + )}

Workspace

From 5961e556e706089e551edeb0e857d67f8dd4c3d4 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Sun, 25 Jun 2023 10:17:23 +0800 Subject: [PATCH 33/54] feat: redirect to overview page after workspace switch (#26) Signed-off-by: Lin Wang --- .../workspace_dropdown_list.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index f7e0113e2c4a..9ad6b7b27e7b 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -57,13 +57,19 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { /** switch the workspace */ setLoading(true); const id = workspaceOption[0].key!; - const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId(window.location.href, id); + const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId( + coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + id + ); if (newUrl) { window.location.href = newUrl; } setLoading(false); }, - [coreStart.workspaces] + [coreStart.workspaces, coreStart.application] ); const onCreateWorkspaceClick = () => { From c4a16bdebe55edd85698ba690aa8ea7f00d6e10c Mon Sep 17 00:00:00 2001 From: raintygao Date: Sun, 25 Jun 2023 11:37:27 +0800 Subject: [PATCH 34/54] update menu filter logic (#28) * feat: update menu logic Signed-off-by: tygao * fix: use navLinks to filter Signed-off-by: tygao --------- Signed-off-by: tygao --- .../chrome/ui/header/collapsible_nav.tsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 549f13997f79..2b7fec106849 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -126,11 +126,11 @@ export function CollapsibleNav({ const appId = useObservable(observables.appId$, ''); const currentWorkspace = useObservable(observables.currentWorkspace$); const lockRef = useRef(null); - const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); + const filterdLinks = getFilterLinks(currentWorkspace, navLinks); + const groupedNavLinks = groupBy(filterdLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; - const filterdLinks = getFilterLinks(currentWorkspace, allCategorizedLinks); - const categoryDictionary = getAllCategories(filterdLinks); - const orderedCategories = getOrderedCategories(filterdLinks, categoryDictionary); + const categoryDictionary = getAllCategories(allCategorizedLinks); + const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { return createEuiListItem({ @@ -152,20 +152,13 @@ export function CollapsibleNav({ function getFilterLinks( workspace: WorkspaceAttribute | null | undefined, - categorizedLinks: Record + allNavLinks: ChromeNavLink[] ) { - // plugins are in this dictionary - const pluginsDictionary = categorizedLinks.opensearch; - if (!pluginsDictionary) return categorizedLinks; + if (!workspace) return allNavLinks; - const features = workspace?.features ?? []; - const newPluginsDictionary = pluginsDictionary.filter((item) => features.indexOf(item.id) > -1); - if (newPluginsDictionary.length === 0) { - delete categorizedLinks.opensearch; - } else { - categorizedLinks.opensearch = newPluginsDictionary; - } - return categorizedLinks; + const features = workspace.features ?? []; + const links = allNavLinks.filter((item) => features.indexOf(item.id) > -1); + return links; } /** From 0b4836c50a07e1caec6b6b082d649a4660f2da32 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Sun, 25 Jun 2023 15:36:28 +0800 Subject: [PATCH 35/54] feat: redirect to workspace overview page after created success (#29) Signed-off-by: Lin Wang --- .../workspace_creator/workspace_creator.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index e4b5a4cf7693..bbea2d2aa0a2 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -10,7 +10,7 @@ import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; import { WorkspaceForm, WorkspaceFormData } from './workspace_form'; -import { WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants'; +import { PATHS, WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_CREATE } from '../../../common/constants'; export const WorkspaceCreator = () => { const { @@ -37,6 +37,15 @@ export const WorkspaceCreator = () => { defaultMessage: 'Create workspace successfully', }), }); + if (application && workspaces) { + window.location.href = workspaces.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + result.result.id + ); + } return; } notifications?.toasts.addDanger({ @@ -46,7 +55,7 @@ export const WorkspaceCreator = () => { text: result?.error, }); }, - [notifications?.toasts, workspaces?.client] + [notifications?.toasts, workspaces, application] ); return ( From 557960bab83138a46a82ae28892d49ddd6ebc632 Mon Sep 17 00:00:00 2001 From: suzhou Date: Sun, 25 Jun 2023 16:27:57 +0800 Subject: [PATCH 36/54] [Feature] Complied saved_objects create/find (#18) * temp: save Signed-off-by: SuZhoue-Joe * feat: make create/find support workspaces Signed-off-by: SuZhoue-Joe * feat: extract management code Signed-off-by: SuZhoue-Joe * fix: type check Signed-off-by: SuZhoue-Joe * fix: build error Signed-off-by: SuZhoue-Joe * feat: enable workspaces on saved client server side Signed-off-by: SuZhoue-Joe * feat: some optimization Signed-off-by: SuZhoue-Joe * feat: extract management code Signed-off-by: SuZhoue-Joe * feat: merge fix Signed-off-by: SuZhoue-Joe * feat: optimize code Signed-off-by: SuZhoue-Joe * feat: reuse common function Signed-off-by: SuZhoue-Joe * feat: optimize code when create Signed-off-by: SuZhoue-Joe * feat: remove useless test code Signed-off-by: SuZhoue-Joe --------- Signed-off-by: SuZhoue-Joe --- .../saved_objects/saved_objects_client.ts | 33 +++++++++++++-- .../export/get_sorted_objects_for_export.ts | 9 +++- .../import/create_saved_objects.ts | 5 ++- .../import/import_saved_objects.ts | 2 + src/core/server/saved_objects/import/types.ts | 2 + .../migrations/core/build_active_mappings.ts | 3 ++ .../saved_objects/routes/bulk_create.ts | 8 +++- .../server/saved_objects/routes/create.ts | 12 +++++- .../server/saved_objects/routes/export.ts | 11 ++++- src/core/server/saved_objects/routes/find.ts | 4 ++ .../server/saved_objects/routes/import.ts | 6 ++- src/core/server/saved_objects/routes/utils.ts | 18 +++++++- .../saved_objects/serialization/serializer.ts | 5 ++- .../saved_objects/serialization/types.ts | 2 + .../saved_objects/service/lib/repository.ts | 27 ++++++++++-- .../service/lib/search_dsl/query_params.ts | 42 +++++++++++++++++++ .../service/lib/search_dsl/search_dsl.ts | 3 ++ src/core/server/saved_objects/types.ts | 2 + src/plugins/workspace/public/plugin.ts | 11 +++++ 19 files changed, 189 insertions(+), 16 deletions(-) diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index d43b75b2171d..bd5f82223306 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -42,6 +42,7 @@ import { import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; +import { WorkspacesStart } from '../workspace'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, @@ -61,6 +62,7 @@ export interface SavedObjectsCreateOptions { /** {@inheritDoc SavedObjectsMigrationVersion} */ migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; + workspaces?: string[]; } /** @@ -183,6 +185,7 @@ const getObjectsToFetch = (queue: BatchQueueEntry[]): ObjectTypeAndId[] => { export class SavedObjectsClient { private http: HttpSetup; private batchQueue: BatchQueueEntry[]; + private currentWorkspaceId?: string; /** * Throttled processing of get requests into bulk requests at 100ms interval @@ -227,6 +230,15 @@ export class SavedObjectsClient { this.batchQueue = []; } + private async _getCurrentWorkspace(): Promise { + return this.currentWorkspaceId || null; + } + + public async setCurrentWorkspace(workspaceId: string): Promise { + this.currentWorkspaceId = workspaceId; + return true; + } + /** * Persists an object * @@ -235,7 +247,7 @@ export class SavedObjectsClient { * @param options * @returns */ - public create = ( + public create = async ( type: string, attributes: T, options: SavedObjectsCreateOptions = {} @@ -248,6 +260,7 @@ export class SavedObjectsClient { const query = { overwrite: options.overwrite, }; + const currentWorkspaceId = await this._getCurrentWorkspace(); const createRequest: Promise> = this.savedObjectsFetch(path, { method: 'POST', @@ -256,6 +269,11 @@ export class SavedObjectsClient { attributes, migrationVersion: options.migrationVersion, references: options.references, + ...(options.workspaces || currentWorkspaceId + ? { + workspaces: options.workspaces || [currentWorkspaceId], + } + : {}), }), }); @@ -328,7 +346,7 @@ export class SavedObjectsClient { * @property {object} [options.hasReference] - { type, id } * @returns A find result with objects matching the specified search. */ - public find = ( + public find = async ( options: SavedObjectsFindOptions ): Promise> => { const path = this.getPath(['_find']); @@ -345,9 +363,18 @@ export class SavedObjectsClient { filter: 'filter', namespaces: 'namespaces', preference: 'preference', + workspaces: 'workspaces', }; - const renamedQuery = renameKeys(renameMap, options); + const workspaces = [ + ...(options.workspaces || [await this._getCurrentWorkspace()]), + 'public', + ].filter((item) => item); + + const renamedQuery = renameKeys(renameMap, { + ...options, + workspaces, + }); const query = pick.apply(null, [renamedQuery, ...Object.values(renameMap)]); const request: ReturnType = this.savedObjectsFetch(path, { diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 7bf6e9f6ccdc..8ca085639f10 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -60,6 +60,8 @@ export interface SavedObjectsExportOptions { excludeExportDetails?: boolean; /** optional namespace to override the namespace used by the savedObjectsClient. */ namespace?: string; + /** optional workspaces to override the workspaces used by the savedObjectsClient. */ + workspaces?: string[]; } /** @@ -87,6 +89,7 @@ async function fetchObjectsToExport({ exportSizeLimit, savedObjectsClient, namespace, + workspaces, }: { objects?: SavedObjectsExportOptions['objects']; types?: string[]; @@ -94,6 +97,7 @@ async function fetchObjectsToExport({ exportSizeLimit: number; savedObjectsClient: SavedObjectsClientContract; namespace?: string; + workspaces?: string[]; }) { if ((types?.length ?? 0) > 0 && (objects?.length ?? 0) > 0) { throw Boom.badRequest(`Can't specify both "types" and "objects" properties when exporting`); @@ -105,7 +109,7 @@ async function fetchObjectsToExport({ if (typeof search === 'string') { throw Boom.badRequest(`Can't specify both "search" and "objects" properties when exporting`); } - const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace }); + const bulkGetResult = await savedObjectsClient.bulkGet(objects, { namespace, workspaces }); const erroredObjects = bulkGetResult.saved_objects.filter((obj) => !!obj.error); if (erroredObjects.length) { const err = Boom.badRequest(); @@ -121,6 +125,7 @@ async function fetchObjectsToExport({ search, perPage: exportSizeLimit, namespaces: namespace ? [namespace] : undefined, + workspaces, }); if (findResponse.total > exportSizeLimit) { throw Boom.badRequest(`Can't export more than ${exportSizeLimit} objects`); @@ -153,6 +158,7 @@ export async function exportSavedObjectsToStream({ includeReferencesDeep = false, excludeExportDetails = false, namespace, + workspaces, }: SavedObjectsExportOptions) { const rootObjects = await fetchObjectsToExport({ types, @@ -161,6 +167,7 @@ export async function exportSavedObjectsToStream({ savedObjectsClient, exportSizeLimit, namespace, + workspaces, }); let exportedObjects: Array> = []; let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; diff --git a/src/core/server/saved_objects/import/create_saved_objects.ts b/src/core/server/saved_objects/import/create_saved_objects.ts index a3a1eebbd2ab..b67cffce1e96 100644 --- a/src/core/server/saved_objects/import/create_saved_objects.ts +++ b/src/core/server/saved_objects/import/create_saved_objects.ts @@ -39,6 +39,7 @@ interface CreateSavedObjectsParams { importIdMap: Map; namespace?: string; overwrite?: boolean; + workspaces?: string[]; } interface CreateSavedObjectsResult { createdObjects: Array>; @@ -56,6 +57,7 @@ export const createSavedObjects = async ({ importIdMap, namespace, overwrite, + workspaces, }: CreateSavedObjectsParams): Promise> => { // filter out any objects that resulted in errors const errorSet = accumulatedErrors.reduce( @@ -103,6 +105,7 @@ export const createSavedObjects = async ({ const bulkCreateResponse = await savedObjectsClient.bulkCreate(objectsToCreate, { namespace, overwrite, + workspaces, }); expectedResults = bulkCreateResponse.saved_objects; } @@ -110,7 +113,7 @@ export const createSavedObjects = async ({ // remap results to reflect the object IDs that were submitted for import // this ensures that consumers understand the results const remappedResults = expectedResults.map>((result) => { - const { id } = objectIdMap.get(`${result.type}:${result.id}`)!; + const { id } = objectIdMap.get(`${result.type}:${result.id}`) || ({} as SavedObject); // also, include a `destinationId` field if the object create attempt was made with a different ID return { ...result, id, ...(id !== result.id && { destinationId: result.id }) }; }); diff --git a/src/core/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts index cd250fc5f65f..68104db85e6f 100644 --- a/src/core/server/saved_objects/import/import_saved_objects.ts +++ b/src/core/server/saved_objects/import/import_saved_objects.ts @@ -54,6 +54,7 @@ export async function importSavedObjectsFromStream({ savedObjectsClient, typeRegistry, namespace, + workspaces, }: SavedObjectsImportOptions): Promise { let errorAccumulator: SavedObjectsImportError[] = []; const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((type) => type.name); @@ -118,6 +119,7 @@ export async function importSavedObjectsFromStream({ importIdMap, overwrite, namespace, + workspaces, }; const createSavedObjectsResult = await createSavedObjects(createSavedObjectsParams); errorAccumulator = [...errorAccumulator, ...createSavedObjectsResult.errors]; diff --git a/src/core/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts index 88beacb9d2fd..ab13fbfe4658 100644 --- a/src/core/server/saved_objects/import/types.ts +++ b/src/core/server/saved_objects/import/types.ts @@ -187,6 +187,8 @@ export interface SavedObjectsImportOptions { namespace?: string; /** If true, will create new copies of import objects, each with a random `id` and undefined `originId`. */ createNewCopies: boolean; + /** if specified, will import in given workspaces, else will import as global object */ + workspaces?: string[]; } /** diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index bf377a13a42e..812cc1fd5eb1 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -175,6 +175,9 @@ function defaultMapping(): IndexMapping { }, }, }, + workspaces: { + type: 'keyword', + }, }, }; } diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 5c2844d64813..61a458d9a618 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -30,6 +30,7 @@ import { schema } from '@osd/config-schema'; import { IRouter } from '../../http'; +import { formatWorkspaces, workspacesValidator } from './utils'; export const registerBulkCreateRoute = (router: IRouter) => { router.post( @@ -38,6 +39,7 @@ export const registerBulkCreateRoute = (router: IRouter) => { validate: { query: schema.object({ overwrite: schema.boolean({ defaultValue: false }), + workspaces: workspacesValidator, }), body: schema.arrayOf( schema.object({ @@ -62,7 +64,11 @@ export const registerBulkCreateRoute = (router: IRouter) => { }, router.handleLegacyErrors(async (context, req, res) => { const { overwrite } = req.query; - const result = await context.core.savedObjects.client.bulkCreate(req.body, { overwrite }); + const workspaces = formatWorkspaces(req.query.workspaces); + const result = await context.core.savedObjects.client.bulkCreate(req.body, { + overwrite, + workspaces, + }); return res.ok({ body: result }); }) ); diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index c8c330ba7774..4d22bd244a03 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -56,15 +56,23 @@ export const registerCreateRoute = (router: IRouter) => { ) ), initialNamespaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + workspaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), }), }, }, router.handleLegacyErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; - const { attributes, migrationVersion, references, initialNamespaces } = req.body; + const { attributes, migrationVersion, references, initialNamespaces, workspaces } = req.body; - const options = { id, overwrite, migrationVersion, references, initialNamespaces }; + const options = { + id, + overwrite, + migrationVersion, + references, + initialNamespaces, + workspaces, + }; const result = await context.core.savedObjects.client.create(type, attributes, options); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 2c808b731b4e..9325b632e40f 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -57,12 +57,20 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) search: schema.maybe(schema.string()), includeReferencesDeep: schema.boolean({ defaultValue: false }), excludeExportDetails: schema.boolean({ defaultValue: false }), + workspaces: schema.maybe(schema.arrayOf(schema.string())), }), }, }, router.handleLegacyErrors(async (context, req, res) => { const savedObjectsClient = context.core.savedObjects.client; - const { type, objects, search, excludeExportDetails, includeReferencesDeep } = req.body; + const { + type, + objects, + search, + excludeExportDetails, + includeReferencesDeep, + workspaces, + } = req.body; const types = typeof type === 'string' ? [type] : type; // need to access the registry for type validation, can't use the schema for this @@ -98,6 +106,7 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) exportSizeLimit: maxImportExportSize, includeReferencesDeep, excludeExportDetails, + workspaces, }); const docsToExport: string[] = await createPromiseFromStreams([ diff --git a/src/core/server/saved_objects/routes/find.ts b/src/core/server/saved_objects/routes/find.ts index dbc9bf9e3a0d..447ec8f6d7de 100644 --- a/src/core/server/saved_objects/routes/find.ts +++ b/src/core/server/saved_objects/routes/find.ts @@ -30,6 +30,7 @@ import { schema } from '@osd/config-schema'; import { IRouter } from '../../http'; +import { formatWorkspaces, workspacesValidator } from './utils'; export const registerFindRoute = (router: IRouter) => { router.get( @@ -59,6 +60,7 @@ export const registerFindRoute = (router: IRouter) => { namespaces: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) ), + workspaces: workspacesValidator, }), }, }, @@ -67,6 +69,7 @@ export const registerFindRoute = (router: IRouter) => { const namespaces = typeof req.query.namespaces === 'string' ? [req.query.namespaces] : req.query.namespaces; + const workspaces = formatWorkspaces(query.workspaces); const result = await context.core.savedObjects.client.find({ perPage: query.per_page, @@ -81,6 +84,7 @@ export const registerFindRoute = (router: IRouter) => { fields: typeof query.fields === 'string' ? [query.fields] : query.fields, filter: query.filter, namespaces, + workspaces, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index b157feb0860e..794f8ef84a79 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -34,7 +34,7 @@ import { schema } from '@osd/config-schema'; import { IRouter } from '../../http'; import { importSavedObjectsFromStream } from '../import'; import { SavedObjectConfig } from '../saved_objects_config'; -import { createSavedObjectsStreamFromNdJson } from './utils'; +import { createSavedObjectsStreamFromNdJson, formatWorkspaces, workspacesValidator } from './utils'; interface FileStream extends Readable { hapi: { @@ -60,6 +60,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) { overwrite: schema.boolean({ defaultValue: false }), createNewCopies: schema.boolean({ defaultValue: false }), + workspaces: workspacesValidator, }, { validate: (object) => { @@ -91,6 +92,8 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) }); } + const workspaces = formatWorkspaces(req.query.workspaces); + const result = await importSavedObjectsFromStream({ savedObjectsClient: context.core.savedObjects.client, typeRegistry: context.core.savedObjects.typeRegistry, @@ -98,6 +101,7 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) objectLimit: maxImportExportSize, overwrite, createNewCopies, + workspaces, }); return res.ok({ body: result }); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index a4c9375e4716..c2b77655ff18 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -27,7 +27,7 @@ * specific language governing permissions and limitations * under the License. */ - +import { schema } from '@osd/config-schema'; import { Readable } from 'stream'; import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; import { @@ -74,3 +74,19 @@ export function validateObjects( .join(', ')}`; } } + +export const workspacesValidator = schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) +); + +export function formatWorkspaces(workspaces?: string | string[]): string[] | undefined { + if (Array.isArray(workspaces)) { + return workspaces; + } + + if (!workspaces) { + return undefined; + } + + return [workspaces]; +} diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index ff840a1fac60..5c3e22ac646a 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -73,7 +73,7 @@ export class SavedObjectsSerializer { */ public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace, namespaces, originId } = _source; + const { type, namespace, namespaces, originId, workspaces } = _source; const version = _seq_no != null || _primary_term != null @@ -91,6 +91,7 @@ export class SavedObjectsSerializer { ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), ...(_source.updated_at && { updated_at: _source.updated_at }), ...(version && { version }), + ...(workspaces && { workspaces }), }; } @@ -112,6 +113,7 @@ export class SavedObjectsSerializer { updated_at, version, references, + workspaces, } = savedObj; const source = { [type]: attributes, @@ -122,6 +124,7 @@ export class SavedObjectsSerializer { ...(originId && { originId }), ...(migrationVersion && { migrationVersion }), ...(updated_at && { updated_at }), + ...(workspaces && { workspaces }), }; return { diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index d10ec75cdf41..473a63cf65f4 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -52,6 +52,7 @@ export interface SavedObjectsRawDocSource { updated_at?: string; references?: SavedObjectReference[]; originId?: string; + workspaces?: string[]; [typeMapping: string]: any; } @@ -69,6 +70,7 @@ interface SavedObjectDoc { version?: string; updated_at?: string; originId?: string; + workspaces?: string[]; } interface Referencable { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index bccfd8ff2265..4a8ceb5e0b3b 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -243,6 +243,7 @@ export class SavedObjectsRepository { originId, initialNamespaces, version, + workspaces, } = options; const namespace = normalizeNamespace(options.namespace); @@ -289,6 +290,7 @@ export class SavedObjectsRepository { migrationVersion, updated_at: time, ...(Array.isArray(references) && { references }), + ...(Array.isArray(workspaces) && { workspaces }), }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); @@ -402,6 +404,16 @@ export class SavedObjectsRepository { object: { initialNamespaces, version, ...object }, method, } = expectedBulkGetResult.value; + let savedObjectWorkspaces: string[] | undefined; + if (expectedBulkGetResult.value.method === 'create') { + if (options.workspaces) { + savedObjectWorkspaces = Array.from(new Set([...(options.workspaces || [])])); + } + } else if (object.workspaces) { + savedObjectWorkspaces = Array.from( + new Set([...object.workspaces, ...(options.workspaces || [])]) + ); + } if (opensearchRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound @@ -452,6 +464,7 @@ export class SavedObjectsRepository { updated_at: time, references: object.references || [], originId: object.originId, + workspaces: savedObjectWorkspaces, }) as SavedObjectSanitizedDoc ), }; @@ -736,6 +749,7 @@ export class SavedObjectsRepository { typeToNamespacesMap, filter, preference, + workspaces, } = options; if (!type && !typeToNamespacesMap) { @@ -809,6 +823,7 @@ export class SavedObjectsRepository { typeToNamespacesMap, hasReference, kueryNode, + workspaces, }), }, }; @@ -976,7 +991,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { originId, updated_at: updatedAt } = body._source; + const { originId, updated_at: updatedAt, workspaces } = body._source; let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { @@ -991,6 +1006,7 @@ export class SavedObjectsRepository { namespaces, ...(originId && { originId }), ...(updatedAt && { updated_at: updatedAt }), + ...(workspaces && { workspaces }), version: encodeHitVersion(body), attributes: body._source[type], references: body._source.references || [], @@ -1055,7 +1071,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { originId } = body.get?._source ?? {}; + const { originId, workspaces } = body.get?._source ?? {}; let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { namespaces = body.get?._source.namespaces ?? [ @@ -1070,6 +1086,7 @@ export class SavedObjectsRepository { version: encodeHitVersion(body), namespaces, ...(originId && { originId }), + ...(workspaces && { workspaces }), references, attributes, }; @@ -1452,12 +1469,13 @@ export class SavedObjectsRepository { }; } - const { originId } = get._source; + const { originId, workspaces } = get._source; return { id, type, ...(namespaces && { namespaces }), ...(originId && { originId }), + ...(workspaces && { workspaces }), updated_at, version: encodeVersion(seqNo, primaryTerm), attributes, @@ -1754,7 +1772,7 @@ function getSavedObjectFromSource( id: string, doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } ): SavedObject { - const { originId, updated_at: updatedAt } = doc._source; + const { originId, updated_at: updatedAt, workspaces } = doc._source; let namespaces: string[] = []; if (!registry.isNamespaceAgnostic(type)) { @@ -1769,6 +1787,7 @@ function getSavedObjectFromSource( namespaces, ...(originId && { originId }), ...(updatedAt && { updated_at: updatedAt }), + ...(workspaces && { workspaces }), version: encodeHitVersion(doc), attributes: doc._source[type], references: doc._source.references || [], diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 5bbb0a1fe24f..5a2aae5943a6 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -128,6 +128,35 @@ function getClauseForType( }; } +/** + * Gets the clause that will filter for the workspace. + */ +function getClauseForWorkspace(workspace: string) { + if (workspace === '*') { + return { + bool: { + must: { + match_all: {}, + }, + }, + }; + } + + if (workspace === 'public') { + return { + bool: { + must_not: [{ exists: { field: 'workspaces' } }], + }, + }; + } + + return { + bool: { + must: [{ term: { workspaces: workspace } }], + }, + }; +} + interface HasReferenceQueryParams { type: string; id: string; @@ -144,6 +173,7 @@ interface QueryParams { defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; + workspaces?: string[]; } export function getClauseForReference(reference: HasReferenceQueryParams) { @@ -200,6 +230,7 @@ export function getQueryParams({ defaultSearchOperator, hasReference, kueryNode, + workspaces, }: QueryParams) { const types = getTypes( registry, @@ -224,6 +255,17 @@ export function getQueryParams({ ], }; + if (workspaces) { + bool.filter.push({ + bool: { + should: workspaces.map((workspace) => { + return getClauseForWorkspace(workspace); + }), + minimum_should_match: 1, + }, + }); + } + if (search) { const useMatchPhrasePrefix = shouldUseMatchPhrasePrefix(search); const simpleQueryStringClause = getSimpleQueryStringClause({ diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 8b54141a4c3c..df6109eb9d0a 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -52,6 +52,7 @@ interface GetSearchDslOptions { id: string; }; kueryNode?: KueryNode; + workspaces?: string[]; } export function getSearchDsl( @@ -71,6 +72,7 @@ export function getSearchDsl( typeToNamespacesMap, hasReference, kueryNode, + workspaces, } = options; if (!type) { @@ -93,6 +95,7 @@ export function getSearchDsl( defaultSearchOperator, hasReference, kueryNode, + workspaces, }), ...getSortingParams(mappings, type, sortField, sortOrder), }; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 3e2553b8ce51..33862cb149fb 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -110,6 +110,7 @@ export interface SavedObjectsFindOptions { typeToNamespacesMap?: Map; /** An optional OpenSearch preference value to be used for the query **/ preference?: string; + workspaces?: string[]; } /** @@ -119,6 +120,7 @@ export interface SavedObjectsFindOptions { export interface SavedObjectsBaseOptions { /** Specify the namespace for this operation */ namespace?: string; + workspaces?: string[]; } /** diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index df601a3f1a29..f570ac9c5ec9 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -100,6 +100,16 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return {}; } + private async _changeSavedObjectCurrentWorkspace() { + const startServices = await this.core?.getStartServices(); + if (startServices) { + const coreStart = startServices[0]; + coreStart.workspaces.client.currentWorkspaceId$.subscribe((currentWorkspaceId) => { + coreStart.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); + }); + } + } + public start(core: CoreStart) { mountDropdownList(core); @@ -108,6 +118,7 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { baseUrl: core.http.basePath.get(), href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.overview }), }); + this._changeSavedObjectCurrentWorkspace(); return {}; } } From 545f5b343a9fd3ac72377c2f0226fbf2345a0b96 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Sun, 25 Jun 2023 16:45:34 +0800 Subject: [PATCH 37/54] feat: redirect to workspace update page after workspace switch (#30) --- .../workspace_dropdown_list/workspace_dropdown_list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index 9ad6b7b27e7b..cd9e7b829d69 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -59,7 +59,7 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { const id = workspaceOption[0].key!; const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId( coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { - path: PATHS.overview, + path: PATHS.update, absolute: true, }), id From 4c9197904240166f03169b7f6ac5223cc66aa50a Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Sun, 25 Jun 2023 16:49:08 +0800 Subject: [PATCH 38/54] Move delete button to update page (#27) * add delete workspace modal Signed-off-by: yuye-aws * implement delete on workspace overview page Signed-off-by: yuye-aws * fix export on delete workspace modal Signed-off-by: yuye-aws * add try catch to handle errors for workspace delete Signed-off-by: yuye-aws * move visibility control to workspace overview page exlusively Signed-off-by: yuye-aws * remove unused import Signed-off-by: yuye-aws * change workspace overview route to workspace update Signed-off-by: yuye-aws * move delete button from workspace overview page to update page Signed-off-by: yuye-aws * remove update button from workspace overview page Signed-off-by: yuye-aws * recover router to workspace overview page Signed-off-by: yuye-aws * change navigation url for workspace overview button on left side panel Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws --- .../public/components/workspace_overview.tsx | 57 +---------------- .../workspace_updater/workspace_updater.tsx | 64 ++++++++++++++++++- src/plugins/workspace/public/plugin.ts | 2 +- 3 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index b0eabce71555..97c9a07092d9 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -10,7 +10,6 @@ import { of } from 'rxjs'; import { i18n } from '@osd/i18n'; import { ApplicationStart } from '../../../../core/public'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { DeleteWorkspaceModal } from './delete_workspace_modal'; import { PATHS } from '../../common/constants'; import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../../common/constants'; @@ -23,43 +22,6 @@ export const WorkspaceOverview = () => { workspaces ? workspaces.client.currentWorkspace$ : of(null) ); - const workspaceId = currentWorkspace?.id; - const workspaceName = currentWorkspace?.name; - const [deleteWorkspaceModalVisible, setDeleteWorkspaceModalVisible] = useState(false); - - const deleteWorkspace = async () => { - if (workspaceId) { - let result; - try { - result = await workspaces?.client.delete(workspaceId); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - return setDeleteWorkspaceModalVisible(false); - } - if (result?.success) { - notifications?.toasts.addSuccess({ - title: i18n.translate('workspace.delete.success', { - defaultMessage: 'Delete workspace successfully', - }), - }); - } else { - notifications?.toasts.addDanger({ - title: i18n.translate('workspace.delete.failed', { - defaultMessage: 'Failed to delete workspace', - }), - text: result?.error, - }); - } - } - setDeleteWorkspaceModalVisible(false); - await application.navigateToApp('home'); - }; - const onUpdateWorkspaceClick = () => { if (!currentWorkspace || !currentWorkspace.id) { notifications?.toasts.addDanger({ @@ -76,25 +38,8 @@ export const WorkspaceOverview = () => { return ( <> - setDeleteWorkspaceModalVisible(true)}> - Delete - , - Update, - Delete, - Update, - ]} - /> + - {deleteWorkspaceModalVisible && ( - setDeleteWorkspaceModalVisible(false)} - selectedItems={workspaceName ? [workspaceName] : []} - /> - )}

Workspace

diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index 2706ee5363d5..f3c8be0abb48 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -4,7 +4,14 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageContent, + EuiButton, + EuiPanel, +} from '@elastic/eui'; import { useObservable } from 'react-use'; import { i18n } from '@osd/i18n'; import { of } from 'rxjs'; @@ -20,6 +27,7 @@ import { WORKSPACE_OP_TYPE_UPDATE, } from '../../../common/constants'; import { ApplicationStart } from '../../../../../core/public'; +import { DeleteWorkspaceModal } from '../delete_workspace_modal'; export const WorkspaceUpdater = () => { const { @@ -34,6 +42,7 @@ export const WorkspaceUpdater = () => { const { [excludedAttribute]: removedProperty, ...otherAttributes } = currentWorkspace || ({} as WorkspaceAttribute); + const [deleteWorkspaceModalVisible, setDeleteWorkspaceModalVisible] = useState(false); const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState< Omit >(otherAttributes); @@ -71,7 +80,7 @@ export const WorkspaceUpdater = () => { defaultMessage: 'Update workspace successfully', }), }); - application.navigateToApp(WORKSPACE_APP_ID, { + await application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.overview + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, }); return; @@ -89,11 +98,51 @@ export const WorkspaceUpdater = () => { if (!currentWorkspaceFormData.name) { return null; } + const deleteWorkspace = async () => { + if (currentWorkspace?.id) { + let result; + try { + result = await workspaces?.client.delete(currentWorkspace?.id); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return setDeleteWorkspaceModalVisible(false); + } + if (result?.success) { + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.delete.success', { + defaultMessage: 'Delete workspace successfully', + }), + }); + } else { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.delete.failed', { + defaultMessage: 'Failed to delete workspace', + }), + text: result?.error, + }); + } + } + setDeleteWorkspaceModalVisible(false); + await application.navigateToApp('home'); + }; return ( - + setDeleteWorkspaceModalVisible(true)}> + Delete + , + ]} + /> { hasShadow={false} style={{ width: '100%', maxWidth: 1000 }} > + {deleteWorkspaceModalVisible && ( + + setDeleteWorkspaceModalVisible(false)} + selectedItems={currentWorkspace?.name ? [currentWorkspace.name] : []} + /> + + )} {application && ( { core.chrome.setCustomNavLink({ title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), baseUrl: core.http.basePath.get(), - href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.overview }), + href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.update }), }); this._changeSavedObjectCurrentWorkspace(); return {}; From cfe506d1997a2d6db7666d8c3d51d4066c1340e6 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Mon, 26 Jun 2023 09:21:23 +0800 Subject: [PATCH 39/54] fix: linting error Signed-off-by: Yulong Ruan --- src/core/public/saved_objects/saved_objects_client.ts | 1 - src/core/public/saved_objects/saved_objects_service.mock.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index bd5f82223306..2ddc776ffdf1 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -42,7 +42,6 @@ import { import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; -import { WorkspacesStart } from '../workspace'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, diff --git a/src/core/public/saved_objects/saved_objects_service.mock.ts b/src/core/public/saved_objects/saved_objects_service.mock.ts index 47bd146058f7..00ca44072958 100644 --- a/src/core/public/saved_objects/saved_objects_service.mock.ts +++ b/src/core/public/saved_objects/saved_objects_service.mock.ts @@ -41,6 +41,7 @@ const createStartContractMock = () => { find: jest.fn(), get: jest.fn(), update: jest.fn(), + setCurrentWorkspace: jest.fn(), }, }; return mock; From 704818fd9162528ef8e6d18a1805dbe0fa292ff8 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 26 Jun 2023 15:51:38 +0800 Subject: [PATCH 40/54] remove duplicate EuiPage (#34) * remove duplicate EuiPage Signed-off-by: Hailong Cui * fix: remove duplicate workspace template Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- .../workspace/public/components/workspace_app.tsx | 15 +++++---------- src/plugins/workspace/public/hooks.ts | 10 ++++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_app.tsx b/src/plugins/workspace/public/components/workspace_app.tsx index 54c326bc551f..ae2720d75b30 100644 --- a/src/plugins/workspace/public/components/workspace_app.tsx +++ b/src/plugins/workspace/public/components/workspace_app.tsx @@ -4,7 +4,6 @@ */ import React, { useEffect } from 'react'; -import { EuiPage, EuiPageBody } from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; import { Route, Switch, useLocation } from 'react-router-dom'; @@ -28,15 +27,11 @@ export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { return ( - - - - {ROUTES.map(({ path, Component, exact }) => ( - } exact={exact ?? false} /> - ))} - - - + + {ROUTES.map(({ path, Component, exact }) => ( + } exact={exact ?? false} /> + ))} + ); }; diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 636a00742146..1132aac04e73 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -23,13 +23,14 @@ export function useWorkspaceTemplate(application: ApplicationStart) { const applications = useObservable(application.applications$); return useMemo(() => { + const tempWsTemplates = [] as WorkspaceTemplate[]; let workspaceTemplates = [] as WorkspaceTemplate[]; const templateFeatureMap = new Map(); if (applications) { applications.forEach((app) => { const { workspaceTemplate: templates = [] } = app; - workspaceTemplates.push(...templates); + tempWsTemplates.push(...templates); for (const template of templates) { const features = templateFeatureMap.get(template.id) || []; features.push(app); @@ -37,7 +38,12 @@ export function useWorkspaceTemplate(application: ApplicationStart) { } }); - workspaceTemplates = [...new Set(workspaceTemplates)]; + workspaceTemplates = tempWsTemplates.reduce((list, curr) => { + if (!list.find((ws) => ws.id === curr.id)) { + list.push(curr); + } + return list; + }, [] as WorkspaceTemplate[]); workspaceTemplates.sort((a, b) => (a.order || 0) - (b.order || 0)); } From 19909fe4753fed8a129760913508d1a28c8cf983 Mon Sep 17 00:00:00 2001 From: zhichao-aws Date: Mon, 26 Jun 2023 15:51:56 +0800 Subject: [PATCH 41/54] remove clear button, add the width of create button (#33) Signed-off-by: zhichao-aws --- .../workspace_dropdown_list/workspace_dropdown_list.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index cd9e7b829d69..ce1b8a6053d5 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -90,7 +90,12 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { selectedOptions={currentWorkspaceOption} singleSelection={{ asPlainText: true }} onSearchChange={onSearchChange} - append={Create workspace} + isClearable={false} + append={ + + Create workspace + + } /> ); From 62115d59049ebb5e62ef6866f1dd658f8d03b062 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 27 Jun 2023 22:30:28 +0800 Subject: [PATCH 42/54] rename OpenSearch Plugins to OpenSearch Features this is a temporary fix just for demo, should be reverted later Signed-off-by: Yulong Ruan --- .../components/workspace_creator/workspace_form.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx index e561c3850827..98273625d17a 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -301,9 +301,11 @@ export const WorkspaceForm = ({ ? handleFeatureGroupChange : handleFeatureCheckboxChange } - label={`${featureOrGroup.name}${ - features.length > 0 ? `(${selectedIds.length}/${features.length})` : '' - }`} + label={`${ + featureOrGroup.name === 'OpenSearch Plugins' + ? 'OpenSearch Features' + : featureOrGroup.name + }${features.length > 0 ? `(${selectedIds.length}/${features.length})` : ''}`} checked={selectedIds.length > 0} indeterminate={ isWorkspaceFeatureGroup(featureOrGroup) && From 40ac0a2999492d28aaeafa2db0f82b8ff8657686 Mon Sep 17 00:00:00 2001 From: suzhou Date: Thu, 29 Jun 2023 15:46:56 +0800 Subject: [PATCH 43/54] Add some logic check when overwrite a saved object (#32) * feat: add some logic check when overwrite a saved object Signed-off-by: SuZhoue-Joe * fix: type check Signed-off-by: SuZhoue-Joe * feat: update Signed-off-by: SuZhoue-Joe --------- Signed-off-by: SuZhoue-Joe --- .../server/saved_objects/service/lib/repository.ts | 14 +++++++++++++- src/core/types/saved_objects.ts | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 4a8ceb5e0b3b..b992adc0f24e 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -280,6 +280,18 @@ export class SavedObjectsRepository { } } + let savedObjectWorkspaces; + + if (id && overwrite) { + // do not overwrite workspaces + const currentItem = await this.get(type, id); + if (currentItem && currentItem.workspaces) { + savedObjectWorkspaces = currentItem.workspaces; + } + } else { + savedObjectWorkspaces = workspaces; + } + const migrated = this._migrator.migrateDocument({ id, type, @@ -290,7 +302,7 @@ export class SavedObjectsRepository { migrationVersion, updated_at: time, ...(Array.isArray(references) && { references }), - ...(Array.isArray(workspaces) && { workspaces }), + ...(Array.isArray(savedObjectWorkspaces) && { workspaces: savedObjectWorkspaces }), }); const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 81e1ed029ddc..47faffb0b922 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -113,6 +113,7 @@ export interface SavedObject { * space. */ originId?: string; + workspaces?: string[]; } export interface SavedObjectError { From 49f0bdf3a3ffe619286cba8e2fda3acd8ce23891 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 5 Jul 2023 10:47:18 +0800 Subject: [PATCH 44/54] Add color, icon and defaultVISTheme for workspace (#36) * feat: add color, icon and defaultVISTheme field for workspace saved object Signed-off-by: Lin Wang * add new fields to workspace form Signed-off-by: Lin Wang * feat: remove feature or group name hack Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang --- src/core/server/workspaces/routes/index.ts | 21 +- .../workspaces/saved_objects/workspace.ts | 9 + src/core/server/workspaces/types.ts | 3 + .../workspace_creator/workspace_form.tsx | 179 +++++++++++------- .../workspace_icon_selector.tsx | 36 ++++ 5 files changed, 171 insertions(+), 77 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_creator/workspace_icon_selector.tsx diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts index 980364103ba8..b00da3c3f38c 100644 --- a/src/core/server/workspaces/routes/index.ts +++ b/src/core/server/workspaces/routes/index.ts @@ -9,6 +9,15 @@ import { IWorkspaceDBImpl } from '../types'; const WORKSPACES_API_BASE_URL = '/api/workspaces'; +const workspaceAttributesSchema = schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), + color: schema.maybe(schema.string()), + icon: schema.maybe(schema.string()), + defaultVISTheme: schema.maybe(schema.string()), +}); + export function registerRoutes({ client, logger, @@ -72,11 +81,7 @@ export function registerRoutes({ path: '', validate: { body: schema.object({ - attributes: schema.object({ - description: schema.maybe(schema.string()), - name: schema.string(), - features: schema.maybe(schema.arrayOf(schema.string())), - }), + attributes: workspaceAttributesSchema, }), }, }, @@ -102,11 +107,7 @@ export function registerRoutes({ id: schema.string(), }), body: schema.object({ - attributes: schema.object({ - description: schema.maybe(schema.string()), - name: schema.string(), - features: schema.maybe(schema.arrayOf(schema.string())), - }), + attributes: workspaceAttributesSchema, }), }, }, diff --git a/src/core/server/workspaces/saved_objects/workspace.ts b/src/core/server/workspaces/saved_objects/workspace.ts index e3fbaa0dad6a..d73a9e3b7605 100644 --- a/src/core/server/workspaces/saved_objects/workspace.ts +++ b/src/core/server/workspaces/saved_objects/workspace.ts @@ -41,6 +41,15 @@ export const workspace: SavedObjectsType = { features: { type: 'text', }, + color: { + type: 'text', + }, + icon: { + type: 'text', + }, + defaultVISTheme: { + type: 'text', + }, }, }, }; diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts index e098b4905a1f..532a69ab9ce9 100644 --- a/src/core/server/workspaces/types.ts +++ b/src/core/server/workspaces/types.ts @@ -15,6 +15,9 @@ export interface WorkspaceAttribute { name: string; description?: string; features?: string[]; + color?: string; + icon?: string; + defaultVISTheme?: string; } export interface WorkspaceFindOptions { diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx index 98273625d17a..8e83b7057245 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -20,19 +20,23 @@ import { EuiFlexGrid, EuiFlexGroup, EuiImage, - EuiAccordion, EuiCheckbox, EuiCheckboxGroup, EuiCheckableCardProps, EuiCheckboxGroupProps, EuiCheckboxProps, EuiFieldTextProps, + EuiColorPicker, + EuiColorPickerProps, + EuiComboBox, + EuiComboBoxProps, } from '@elastic/eui'; import { WorkspaceTemplate } from '../../../../../core/types'; import { AppNavLinkStatus, ApplicationStart } from '../../../../../core/public'; import { useApplications, useWorkspaceTemplate } from '../../hooks'; import { WORKSPACE_OP_TYPE_CREATE, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; +import { WorkspaceIconSelector } from './workspace_icon_selector'; interface WorkspaceFeature { id: string; @@ -49,6 +53,9 @@ export interface WorkspaceFormData { name: string; description?: string; features: string[]; + color?: string; + icon?: string; + defaultVISTheme?: string; } type WorkspaceFormErrors = { [key in keyof WorkspaceFormData]?: string }; @@ -59,6 +66,8 @@ const isWorkspaceFeatureGroup = ( const workspaceHtmlIdGenerator = htmlIdGenerator(); +const defaultVISThemeOptions = [{ label: 'Categorical', value: 'categorical' }]; + interface WorkspaceFormProps { application: ApplicationStart; onSubmit?: (formData: WorkspaceFormData) => void; @@ -76,6 +85,10 @@ export const WorkspaceForm = ({ const [name, setName] = useState(defaultValues?.name); const [description, setDescription] = useState(defaultValues?.description); + const [color, setColor] = useState(defaultValues?.color); + const [icon, setIcon] = useState(defaultValues?.icon); + const [defaultVISTheme, setDefaultVISTheme] = useState(defaultValues?.defaultVISTheme); + const [selectedTemplateId, setSelectedTemplateId] = useState(); const [selectedFeatureIds, setSelectedFeatureIds] = useState(defaultValues?.features || []); const selectedTemplate = workspaceTemplates.find( @@ -87,6 +100,9 @@ export const WorkspaceForm = ({ name, description, features: selectedFeatureIds, + color, + icon, + defaultVISTheme, }); const getFormDataRef = useRef(getFormData); getFormDataRef.current = getFormData; @@ -120,6 +136,11 @@ export const WorkspaceForm = ({ }, []); }, [applications]); + const selectedDefaultVISThemeOptions = useMemo( + () => defaultVISThemeOptions.filter((item) => item.value === defaultVISTheme), + [defaultVISTheme] + ); + if (!formIdRef.current) { formIdRef.current = workspaceHtmlIdGenerator(); } @@ -198,6 +219,20 @@ export const WorkspaceForm = ({ setDescription(e.target.value); }, []); + const handleColorChange = useCallback['onChange']>((text) => { + setColor(text); + }, []); + + const handleIconChange = useCallback((newIcon: string) => { + setIcon(newIcon); + }, []); + + const handleDefaultVISThemeInputChange = useCallback< + Required>['onChange'] + >((options) => { + setDefaultVISTheme(options[0]?.value); + }, []); + return ( @@ -217,6 +252,25 @@ export const WorkspaceForm = ({ > + + + + + + + + + @@ -267,74 +321,65 @@ export const WorkspaceForm = ({ )} - - -

Advanced Options

-
- - } - > - - {featureOrGroups.map((featureOrGroup) => { - const features = isWorkspaceFeatureGroup(featureOrGroup) +
+ + + +

Workspace features

+
+ + {featureOrGroups.map((featureOrGroup) => { + const features = isWorkspaceFeatureGroup(featureOrGroup) ? featureOrGroup.features : []; + const selectedIds = selectedFeatureIds.filter((id) => + (isWorkspaceFeatureGroup(featureOrGroup) ? featureOrGroup.features - : []; - const selectedIds = selectedFeatureIds.filter((id) => - (isWorkspaceFeatureGroup(featureOrGroup) - ? featureOrGroup.features - : [featureOrGroup] - ).find((item) => item.id === id) - ); - return ( - - 0 ? `(${selectedIds.length}/${features.length})` : ''}`} - checked={selectedIds.length > 0} - indeterminate={ - isWorkspaceFeatureGroup(featureOrGroup) && - selectedIds.length > 0 && - selectedIds.length < features.length - } + : [featureOrGroup] + ).find((item) => item.id === id) + ); + return ( + + 0 ? `(${selectedIds.length}/${features.length})` : '' + }`} + checked={selectedIds.length > 0} + indeterminate={ + isWorkspaceFeatureGroup(featureOrGroup) && + selectedIds.length > 0 && + selectedIds.length < features.length + } + /> + {isWorkspaceFeatureGroup(featureOrGroup) && ( + ({ + id: item.id, + label: item.name, + }))} + idToSelectedMap={selectedIds.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue]: true, + }), + {} + )} + onChange={handleFeatureChange} + style={{ marginLeft: 40 }} /> - {isWorkspaceFeatureGroup(featureOrGroup) && ( - ({ - id: item.id, - label: item.name, - }))} - idToSelectedMap={selectedIds.reduce( - (previousValue, currentValue) => ({ - ...previousValue, - [currentValue]: true, - }), - {} - )} - onChange={handleFeatureChange} - style={{ marginLeft: 40 }} - /> - )} - - ); - })} - - + )} + + ); + })} +
diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_icon_selector.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_icon_selector.tsx new file mode 100644 index 000000000000..80e08d8e2e98 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_icon_selector.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; + +const icons = ['glasses', 'search', 'bell']; + +export const WorkspaceIconSelector = ({ + color, + value, + onChange, +}: { + color?: string; + value?: string; + onChange: (value: string) => void; +}) => { + return ( + + {icons.map((item) => ( + { + onChange(item); + }} + grow={false} + > + + + ))} + + ); +}; From 72afa20c2364cbf167f07b796e6d1edbf74b4a53 Mon Sep 17 00:00:00 2001 From: raintygao Date: Thu, 6 Jul 2023 16:05:19 +0800 Subject: [PATCH 45/54] feat: add workspace list (#39) Signed-off-by: tygao --- src/plugins/workspace/common/constants.ts | 1 + .../workspace/public/components/routes.ts | 6 + .../public/components/utils/workspace.ts | 22 +++ .../components/workspace_list/index.tsx | 127 ++++++++++++++++++ .../workspace_dropdown_list.tsx | 14 +- 5 files changed, 159 insertions(+), 11 deletions(-) create mode 100644 src/plugins/workspace/public/components/utils/workspace.ts create mode 100644 src/plugins/workspace/public/components/workspace_list/index.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 903f028539dd..2f67640bcd3f 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -11,6 +11,7 @@ export const PATHS = { create: '/create', overview: '/overview', update: '/update', + list: '/list', }; export const WORKSPACE_OP_TYPE_CREATE = 'create'; export const WORKSPACE_OP_TYPE_UPDATE = 'update'; diff --git a/src/plugins/workspace/public/components/routes.ts b/src/plugins/workspace/public/components/routes.ts index 33f7b4774713..9c2d568db021 100644 --- a/src/plugins/workspace/public/components/routes.ts +++ b/src/plugins/workspace/public/components/routes.ts @@ -8,6 +8,7 @@ import { PATHS } from '../../common/constants'; import { WorkspaceCreator } from './workspace_creator'; import { WorkspaceUpdater } from './workspace_updater'; import { WorkspaceOverview } from './workspace_overview'; +import { WorkspaceList } from './workspace_list'; export interface RouteConfig { path: string; @@ -32,4 +33,9 @@ export const ROUTES: RouteConfig[] = [ Component: WorkspaceUpdater, label: 'Update', }, + { + path: PATHS.list, + Component: WorkspaceList, + label: 'List', + }, ]; diff --git a/src/plugins/workspace/public/components/utils/workspace.ts b/src/plugins/workspace/public/components/utils/workspace.ts new file mode 100644 index 000000000000..7ad9a43bf72c --- /dev/null +++ b/src/plugins/workspace/public/components/utils/workspace.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; +import { CoreStart } from '../../../../../core/public'; + +type Core = Pick; + +export const switchWorkspace = ({ workspaces, application }: Core, id: string) => { + const newUrl = workspaces?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.update, + absolute: true, + }), + id + ); + if (newUrl) { + window.location.href = newUrl; + } +}; diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx new file mode 100644 index 000000000000..4568836b87af --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -0,0 +1,127 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageContent, + EuiBasicTable, + EuiLink, + Direction, + CriteriaWithPagination, +} from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; +import { useMemo } from 'react'; +import { useCallback } from 'react'; +import { WorkspaceAttribute } from '../../../../../core/public'; + +import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; +import { switchWorkspace } from '../utils/workspace'; + +export const WorkspaceList = () => { + const { + services: { workspaces, application }, + } = useOpenSearchDashboards(); + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState<'name' | 'id'>('name'); + const [sortDirection, setSortDirection] = useState('asc'); + + const workspaceList = useObservable(workspaces!.client.workspaceList$, []); + + const pageOfItems = useMemo(() => { + return workspaceList + .sort((a, b) => { + const compare = a[sortField].localeCompare(b[sortField]); + return sortDirection === 'asc' ? compare : -compare; + }) + .slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); + }, [workspaceList, pageIndex, pageSize, sortField, sortDirection]); + + const handleSwitchWorkspace = useCallback( + (id: string) => { + if (workspaces && application) { + switchWorkspace({ workspaces, application }, id); + } + }, + [workspaces, application] + ); + + const columns = [ + { + field: 'name', + name: 'Name', + sortable: true, + render: (name: string, item: WorkspaceAttribute) => ( + + handleSwitchWorkspace(item.id)}>{name} + + ), + }, + { + field: 'id', + name: 'ID', + sortable: true, + }, + { + field: 'description', + name: 'Description', + truncateText: true, + }, + { + field: 'features', + name: 'Features', + isExpander: true, + hasActions: true, + }, + ]; + + const onTableChange = ({ page, sort }: CriteriaWithPagination) => { + const { field, direction } = sort!; + const { index, size } = page; + + setPageIndex(index); + setPageSize(size); + setSortField(field as 'name' | 'id'); + setSortDirection(direction); + }; + + return ( + + + + + + + + + ); +}; diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index ce1b8a6053d5..3dd50bb5886f 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -9,6 +9,7 @@ import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; +import { switchWorkspace } from '../../components/utils/workspace'; type WorkspaceOption = EuiComboBoxOptionOption; @@ -57,19 +58,10 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { /** switch the workspace */ setLoading(true); const id = workspaceOption[0].key!; - const newUrl = coreStart.workspaces?.formatUrlWithWorkspaceId( - coreStart.application.getUrlForApp(WORKSPACE_APP_ID, { - path: PATHS.update, - absolute: true, - }), - id - ); - if (newUrl) { - window.location.href = newUrl; - } + switchWorkspace(coreStart, id); setLoading(false); }, - [coreStart.workspaces, coreStart.application] + [coreStart] ); const onCreateWorkspaceClick = () => { From 40856f1d651238ce24fadb3e913a190349ffa001 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Fri, 7 Jul 2023 10:59:18 +0800 Subject: [PATCH 46/54] Feature/menu change (#37) * feat: register library menus Signed-off-by: SuZhoue-Joe * feat: some update Signed-off-by: SuZhoue-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhoue-Joe Signed-off-by: SuZhou-Joe --- src/core/utils/default_app_categories.ts | 7 +- .../public/constants.ts | 31 +++++++++ .../management_section/mount_section.tsx | 35 ++++++---- .../objects_table/components/header.tsx | 9 +-- .../objects_table/saved_objects_table.tsx | 8 ++- .../saved_objects_table_page.tsx | 18 +++-- .../saved_objects_management/public/plugin.ts | 69 ++++++++++++++++++- 7 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 src/plugins/saved_objects_management/public/constants.ts diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 3c0920624e1b..61cb1e250863 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -34,11 +34,10 @@ import { AppCategory } from '../types'; /** @internal */ export const DEFAULT_APP_CATEGORIES: Record = Object.freeze({ opensearchDashboards: { - id: 'opensearchDashboards', - label: i18n.translate('core.ui.opensearchDashboardsNavList.label', { - defaultMessage: 'OpenSearch Dashboards', + id: 'library', + label: i18n.translate('core.ui.libraryNavList.label', { + defaultMessage: 'Library', }), - euiIconType: 'inputOutput', order: 1000, }, enterpriseSearch: { diff --git a/src/plugins/saved_objects_management/public/constants.ts b/src/plugins/saved_objects_management/public/constants.ts new file mode 100644 index 000000000000..dec0d4e7be68 --- /dev/null +++ b/src/plugins/saved_objects_management/public/constants.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; + +export const LIBRARY_OVERVIEW_WORDINGS = i18n.translate('savedObjectsManagement.libraryOverview', { + defaultMessage: 'Overview', +}); + +export const SAVED_OBJECT_MANAGEMENT_TITLE_WORDINGS = i18n.translate( + 'savedObjectsManagement.objectsTable.header.savedObjectsTitle', + { + defaultMessage: 'Saved Objects', + } +); + +export const SAVED_SEARCHES_WORDINGS = i18n.translate( + 'savedObjectsManagement.SearchesManagementSectionLabel', + { + defaultMessage: 'Saved searches', + } +); + +export const SAVED_QUERIES_WORDINGS = i18n.translate( + 'savedObjectsManagement.QueriesManagementSectionLabel', + { + defaultMessage: 'Saved filters', + } +); diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 2c42df5c7824..acc2c5bc8afd 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -32,10 +32,9 @@ import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; import { Router, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@osd/i18n/react'; -import { i18n } from '@osd/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { CoreSetup } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../management/public'; +import { AppMountParameters, CoreSetup } from 'src/core/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; import { ISavedObjectsManagementServiceRegistry } from '../services'; import { getAllowedTypes } from './../lib'; @@ -43,26 +42,32 @@ import { getAllowedTypes } from './../lib'; interface MountParams { core: CoreSetup; serviceRegistry: ISavedObjectsManagementServiceRegistry; - mountParams: ManagementAppMountParams; + mountParams?: ManagementAppMountParams; + appMountParams?: AppMountParameters; + title: string; + allowedObjectTypes?: string[]; + fullWidth?: boolean; } -let allowedObjectTypes: string[] | undefined; - -const title = i18n.translate('savedObjectsManagement.objects.savedObjectsTitle', { - defaultMessage: 'Saved Objects', -}); - const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ core, mountParams, + appMountParams, serviceRegistry, + title, + allowedObjectTypes, + fullWidth = true, }: MountParams) => { const [coreStart, { data }, pluginStart] = await core.getStartServices(); - const { element, history, setBreadcrumbs } = mountParams; - if (allowedObjectTypes === undefined) { - allowedObjectTypes = await getAllowedTypes(coreStart.http); + const usedMountParams = mountParams || appMountParams || ({} as ManagementAppMountParams); + const { element, history } = usedMountParams; + const { chrome } = coreStart; + const setBreadcrumbs = mountParams?.setBreadcrumbs || chrome.setBreadcrumbs; + let finalAllowedObjectTypes = allowedObjectTypes; + if (finalAllowedObjectTypes === undefined) { + finalAllowedObjectTypes = await getAllowedTypes(coreStart.http); } coreStart.chrome.docTitle.change(title); @@ -105,8 +110,10 @@ export const mountManagementSection = async ({ actionRegistry={pluginStart.actions} columnRegistry={pluginStart.columns} namespaceRegistry={pluginStart.namespaces} - allowedTypes={allowedObjectTypes} + allowedTypes={finalAllowedObjectTypes} setBreadcrumbs={setBreadcrumbs} + title={title} + fullWidth={fullWidth} /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx index a22e349d5240..9d46f1cca67c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -45,22 +45,19 @@ export const Header = ({ onImport, onRefresh, filteredCount, + title, }: { onExportAll: () => void; onImport: () => void; onRefresh: () => void; filteredCount: number; + title: string; }) => ( -

- -

+

{title}

diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 2f78f307d165..412047ba66f0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -114,6 +114,8 @@ export interface SavedObjectsTableProps { goInspectObject: (obj: SavedObjectWithMetadata) => void; canGoInApp: (obj: SavedObjectWithMetadata) => boolean; dateFormat: string; + title: string; + fullWidth: boolean; } export interface SavedObjectsTableState { @@ -847,7 +849,10 @@ export class SavedObjectsTable extends Component + {this.renderFlyout()} {this.renderRelationships()} {this.renderDeleteConfirmModal()} @@ -857,6 +862,7 @@ export class SavedObjectsTable extends Component diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index 09937388ba57..ec3837762317 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -30,7 +30,6 @@ import React, { useEffect } from 'react'; import { get } from 'lodash'; -import { i18n } from '@osd/i18n'; import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; import { DataPublicPluginStart } from '../../../data/public'; import { @@ -49,6 +48,8 @@ const SavedObjectsTablePage = ({ columnRegistry, namespaceRegistry, setBreadcrumbs, + title, + fullWidth, }: { coreStart: CoreStart; dataStart: DataPublicPluginStart; @@ -58,6 +59,8 @@ const SavedObjectsTablePage = ({ columnRegistry: SavedObjectsManagementColumnServiceStart; namespaceRegistry: SavedObjectsManagementNamespaceServiceStart; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + title: string; + fullWidth: boolean; }) => { const capabilities = coreStart.application.capabilities; const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); @@ -66,13 +69,14 @@ const SavedObjectsTablePage = ({ useEffect(() => { setBreadcrumbs([ { - text: i18n.translate('savedObjectsManagement.breadcrumb.index', { - defaultMessage: 'Saved objects', - }), - href: '/', + text: title, + /** + * There is no need to set a link for current bread crumb + */ + href: undefined, }, ]); - }, [setBreadcrumbs]); + }, [setBreadcrumbs, title]); return ( ); }; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index ec7d64ed700c..a362e81bf3aa 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -29,7 +29,7 @@ */ import { i18n } from '@osd/i18n'; -import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { VisBuilderStart } from '../../vis_builder/public'; import { ManagementSetup } from '../../management/public'; @@ -52,6 +52,13 @@ import { ISavedObjectsManagementServiceRegistry, } from './services'; import { registerServices } from './register_services'; +import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { + LIBRARY_OVERVIEW_WORDINGS, + SAVED_OBJECT_MANAGEMENT_TITLE_WORDINGS, + SAVED_QUERIES_WORDINGS, + SAVED_SEARCHES_WORDINGS, +} from './constants'; export interface SavedObjectsManagementPluginSetup { actions: SavedObjectsManagementActionServiceSetup; @@ -130,10 +137,70 @@ export class SavedObjectsManagementPlugin core, serviceRegistry: this.serviceRegistry, mountParams, + title: i18n.translate('savedObjectsManagement.managementSectionLabel', { + defaultMessage: 'Saved Objects', + }), }); }, }); + const mountWrapper = ({ + title, + allowedObjectTypes, + }: { + title: string; + allowedObjectTypes?: string[]; + }) => async (appMountParams: AppMountParameters) => { + const { mountManagementSection } = await import('./management_section'); + return mountManagementSection({ + core, + serviceRegistry: this.serviceRegistry, + appMountParams, + title, + allowedObjectTypes, + fullWidth: false, + }); + }; + + /** + * Register saved objects overview & saved search & saved query here + */ + core.application.register({ + id: 'objects_overview', + appRoute: '/app/objects', + exactRoute: true, + title: LIBRARY_OVERVIEW_WORDINGS, + order: 10000, + category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + mount: mountWrapper({ + title: SAVED_OBJECT_MANAGEMENT_TITLE_WORDINGS, + }), + }); + + core.application.register({ + id: 'objects_searches', + appRoute: '/app/objects/search', + title: SAVED_SEARCHES_WORDINGS, + order: 8000, + category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + mount: mountWrapper({ + title: SAVED_SEARCHES_WORDINGS, + allowedObjectTypes: ['search'], + }), + }); + + core.application.register({ + id: 'objects_query', + appRoute: '/app/objects/query', + title: SAVED_QUERIES_WORDINGS, + order: 8001, + category: DEFAULT_APP_CATEGORIES.opensearchDashboards, + mount: mountWrapper({ + title: SAVED_QUERIES_WORDINGS, + allowedObjectTypes: ['query'], + }), + }); + // depends on `getStartServices`, should not be awaited registerServices(this.serviceRegistry, core.getStartServices); From 05ab4f78ac1d208d3f4d26e47ee3f3eccc77d18f Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Fri, 7 Jul 2023 11:08:01 +0800 Subject: [PATCH 47/54] feat: different left menu and exit workspace (#38) * Exit workspace from left menu Signed-off-by: yuye-aws * Show exit workspace button with small window size Signed-off-by: yuye-aws * Remove recently viewed and workspace overview on left menu Signed-off-by: yuye-aws * Add buttons for outside, inside workspace case Signed-off-by: yuye-aws * Implement home button and workspace over view button on left menu Signed-off-by: yuye-aws * Implement workspace dropdown list in left menu Signed-off-by: yuye-aws * Add props on recently accessed and custom nav link Signed-off-by: yuye-aws * Add three props to mock props for collapsible nav: exitWorkspace, getWorkspaceUrl, workspaceList$ Signed-off-by: yuye-aws * Add three props to mock props for header: exitWorkspace, getWorkspaceUrl, workspaceList$ Signed-off-by: yuye-aws * Fix bugs for function createWorkspaceNavLink Signed-off-by: yuye-aws * Remove unused constants Signed-off-by: yuye-aws * Reuse method getWorkspaceUrl Signed-off-by: yuye-aws * Remove recently accessed and custom nav props in test Signed-off-by: yuye-aws * Revert "Remove recently accessed and custom nav props in test" This reverts commit 7895e5c5dcde9e134f26b2d6a3df54a2d62e9274. * Wrap title with i18n Signed-off-by: yuye-aws * Add redirect for workspace app Signed-off-by: yuye-aws * Enable users to go to workspace lists page via see more under workspaces in left menu Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws --- src/core/public/chrome/chrome_service.tsx | 43 +- src/core/public/chrome/constants.ts | 6 + .../chrome/ui/header/collapsible_nav.test.tsx | 3 + .../chrome/ui/header/collapsible_nav.tsx | 383 ++++++++++-------- .../public/chrome/ui/header/header.test.tsx | 3 + src/core/public/chrome/ui/header/header.tsx | 8 + src/core/public/chrome/ui/header/nav_link.tsx | 38 +- .../public/components/workspace_app.tsx | 5 +- src/plugins/workspace/public/plugin.ts | 6 - 9 files changed, 326 insertions(+), 169 deletions(-) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 6b33b77a6f74..ee1cb5363772 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,6 +34,7 @@ import { FormattedMessage } from '@osd/i18n/react'; import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs'; import { flatMap, map, takeUntil } from 'rxjs/operators'; import { EuiLink } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; import { mountReactNode } from '../utils/mount'; import { InternalApplicationStart } from '../application'; import { DocLinksStart } from '../doc_links'; @@ -41,7 +42,7 @@ import { HttpStart } from '../http'; import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; import { IUiSettingsClient } from '../ui_settings'; -import { OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK } from './constants'; +import { OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK, WORKSPACE_APP_ID } from './constants'; import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; @@ -180,6 +181,41 @@ export class ChromeService { docTitle.reset(); }); + const getWorkspaceUrl = (id: string) => { + return workspaces?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: '/', + absolute: true, + }), + id + ); + }; + + const exitWorkspace = async () => { + let result; + try { + result = await workspaces?.client.exitWorkspace(); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.exit.failed', { + defaultMessage: 'Failed to exit workspace', + }), + text: error instanceof Error ? error.message : JSON.stringify(error), + }); + return; + } + if (!result?.success) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.exit.failed', { + defaultMessage: 'Failed to exit workspace', + }), + text: result?.error, + }); + return; + } + await application.navigateToApp('home'); + }; + const setIsNavDrawerLocked = (isLocked: boolean) => { isNavDrawerLocked$.next(isLocked); localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`); @@ -245,7 +281,6 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} - customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} opensearchDashboardsDocLink={docLinks.links.opensearchDashboards.introduction} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))} @@ -254,6 +289,7 @@ export class ChromeService { isVisible$={this.isVisible$} opensearchDashboardsVersion={injectedMetadata.getOpenSearchDashboardsVersion()} navLinks$={navLinks.getNavLinks$()} + customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsCenter$={navControls.getCenter$()} @@ -261,10 +297,13 @@ export class ChromeService { navControlsExpandedCenter$={navControls.getExpandedCenter$()} navControlsExpandedRight$={navControls.getExpandedRight$()} onIsLockedUpdate={setIsNavDrawerLocked} + exitWorkspace={exitWorkspace} + getWorkspaceUrl={getWorkspaceUrl} isLocked$={getIsNavDrawerLocked$} branding={injectedMetadata.getBranding()} survey={injectedMetadata.getSurvey()} currentWorkspace$={workspaces.client.currentWorkspace$} + workspaceList$={workspaces.client.workspaceList$} /> ), diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 5008f8b4a69a..6de7c01f1d13 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -31,3 +31,9 @@ export const OPENSEARCH_DASHBOARDS_ASK_OPENSEARCH_LINK = 'https://forum.opensearch.org/'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose'; + +export const WORKSPACE_APP_ID = 'workspace'; + +export const PATHS = { + list: '/list', +}; diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 4df3f68ec90e..9d9e06ca5673 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -80,8 +80,11 @@ function mockProps() { closeNav: () => {}, navigateToApp: () => Promise.resolve(), navigateToUrl: () => Promise.resolve(), + exitWorkspace: () => {}, + getWorkspaceUrl: (id: string) => '', customNavLink$: new BehaviorSubject(undefined), currentWorkspace$: workspacesServiceMock.createStartContract().client.currentWorkspace$, + workspaceList$: workspacesServiceMock.createStartContract().client.workspaceList$, branding: { darkMode: false, mark: { diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 2b7fec106849..d7d15694b235 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -33,7 +33,6 @@ import { EuiCollapsibleNav, EuiCollapsibleNavGroup, EuiFlexItem, - EuiHorizontalRule, EuiListGroup, EuiListGroupItem, EuiShowFor, @@ -41,17 +40,18 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { groupBy, sortBy } from 'lodash'; -import React, { Fragment, useRef } from 'react'; +import React, { useRef } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; import { AppCategory } from '../../../../types'; -import { InternalApplicationStart } from '../../../application/types'; +import { InternalApplicationStart } from '../../../application'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; +import { createEuiListItem, isModifiedOrPrevented, createWorkspaceNavLink } from './nav_link'; import { ChromeBranding } from '../../chrome_service'; import { WorkspaceAttribute } from '../../../workspace'; +import { WORKSPACE_APP_ID, PATHS } from '../../constants'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -103,7 +103,10 @@ interface Props { navigateToUrl: InternalApplicationStart['navigateToUrl']; customNavLink$: Rx.Observable; branding: ChromeBranding; + exitWorkspace: () => void; + getWorkspaceUrl: (id: string) => string; currentWorkspace$: Rx.BehaviorSubject; + workspaceList$: Rx.BehaviorSubject; } export function CollapsibleNav({ @@ -114,6 +117,8 @@ export function CollapsibleNav({ homeHref, storage = window.localStorage, onIsLockedUpdate, + exitWorkspace, + getWorkspaceUrl, closeNav, navigateToApp, navigateToUrl, @@ -121,13 +126,12 @@ export function CollapsibleNav({ ...observables }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); - const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); - const customNavLink = useObservable(observables.customNavLink$, undefined); const appId = useObservable(observables.appId$, ''); const currentWorkspace = useObservable(observables.currentWorkspace$); + const workspaceList = useObservable(observables.workspaceList$, []).slice(0, 5); const lockRef = useRef(null); - const filterdLinks = getFilterLinks(currentWorkspace, navLinks); - const groupedNavLinks = groupBy(filterdLinks, (link) => link?.category?.id); + const filteredLinks = getFilterLinks(currentWorkspace, navLinks); + const groupedNavLinks = groupBy(filteredLinks, (link) => link?.category?.id); const { undefined: unknowns = [], ...allCategorizedLinks } = groupedNavLinks; const categoryDictionary = getAllCategories(allCategorizedLinks); const orderedCategories = getOrderedCategories(allCategorizedLinks, categoryDictionary); @@ -202,172 +206,235 @@ export function CollapsibleNav({ onClose={closeNav} outsideClickCloses={false} > - {customNavLink && ( - - + + {/* Home, Alerts, Favorites, Projects and Admin outside workspace */} + {!currentWorkspace && ( + <> + { + closeNav(); + await navigateToApp('home'); + }} + iconType={'logoOpenSearch'} + title={i18n.translate('core.ui.primaryNavSection.home', { + defaultMessage: 'Home', + })} + /> + + + +

+ {i18n.translate('core.ui.EmptyFavoriteList', { + defaultMessage: 'No Favorites', + })} +

+
+ +

+ {i18n.translate('core.ui.SeeMoreFavorite', { + defaultMessage: 'SEE MORE', + })} +

+
+
- 0 ? ( + { + const href = getWorkspaceUrl(workspace.id); + const hydratedLink = createWorkspaceNavLink(href, workspace, navLinks); + return { + href, + ...hydratedLink, + 'data-test-subj': 'collapsibleNavAppLink--workspace', + onClick: async (event) => { + if (!isModifiedOrPrevented(event)) { + closeNav(); + } + }, + }; + })} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + /> + ) : ( + +

+ {i18n.translate('core.ui.EmptyWorkspaceList', { + defaultMessage: 'No Workspaces', + })} +

+
+ )} + + color="subdued" + style={{ padding: '0 8px 8px' }} + onClick={async () => { + await navigateToApp(WORKSPACE_APP_ID, { + path: PATHS.list, + }); + }} + > +

+ {i18n.translate('core.ui.SeeMoreWorkspace', { + defaultMessage: 'SEE MORE', + })} +

+
-
- - -
- )} - - {/* Recently viewed */} - setIsCategoryOpen('recentlyViewed', isCategoryOpen, storage)} - data-test-subj="collapsibleNavGroup-recentlyViewed" - > - {recentlyAccessed.length > 0 ? ( - { - // TODO #64541 - // Can remove icon from recent links completely - const { iconType, onClick, ...hydratedLink } = createRecentNavLink( - link, - navLinks, - basePath, - navigateToUrl - ); + + + )} - return { - ...hydratedLink, - 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: (event) => { - if (!isModifiedOrPrevented(event)) { - closeNav(); - onClick(event); - } - }, - }; - })} - maxWidth="none" - color="subdued" - gutterSize="none" - size="s" - className="osdCollapsibleNav__recentsListGroup" - /> - ) : ( - -

- {i18n.translate('core.ui.EmptyRecentlyViewed', { - defaultMessage: 'No recently viewed items', + {/* Workspace name and Overview inside workspace */} + {currentWorkspace && ( + <> + + { + window.location.href = getWorkspaceUrl(currentWorkspace.id); + }} + iconType={'grid'} + title={i18n.translate('core.ui.primaryNavSection.overview', { + defaultMessage: 'Overview', })} -

-
+ /> + )} -
- + {/* OpenSearchDashboards, Observability, Security, and Management sections inside workspace */} + {currentWorkspace && + orderedCategories.map((categoryName) => { + const category = categoryDictionary[categoryName]!; + const opensearchLinkLogo = + category.id === 'opensearchDashboards' ? customSideMenuLogo() : category.euiIconType; - - {/* OpenSearchDashboards, Observability, Security, and Management sections */} - {orderedCategories.map((categoryName) => { - const category = categoryDictionary[categoryName]!; - const opensearchLinkLogo = - category.id === 'opensearchDashboards' ? customSideMenuLogo() : category.euiIconType; + return ( + + setIsCategoryOpen(category.id, isCategoryOpen, storage) + } + data-test-subj={`collapsibleNavGroup-${category.id}`} + data-test-opensearch-logo={opensearchLinkLogo} + > + readyForEUI(link))} + maxWidth="none" + color="subdued" + gutterSize="none" + size="s" + /> + + ); + })} - return ( - setIsCategoryOpen(category.id, isCategoryOpen, storage)} - data-test-subj={`collapsibleNavGroup-${category.id}`} - data-test-opensearch-logo={opensearchLinkLogo} - > - readyForEUI(link))} - maxWidth="none" - color="subdued" - gutterSize="none" - size="s" - /> + {/* Things with no category (largely for custom plugins) inside workspace */} + {currentWorkspace && + unknowns.map((link, i) => ( + + + + - ); - })} - - {/* Things with no category (largely for custom plugins) */} - {unknowns.map((link, i) => ( - - - - - - ))} + ))} - {/* Docking button only for larger screens that can support it*/} - - - + + + {/* Exit workspace button only within a workspace*/} + {currentWorkspace && ( { - onIsLockedUpdate(!isLocked); - if (lockRef.current) { - lockRef.current.focus(); - } - }} - iconType={isLocked ? 'lock' : 'lockOpen'} + label={i18n.translate('core.ui.primaryNavSection.exitWorkspaceLabel', { + defaultMessage: 'Exit workspace', + })} + aria-label={i18n.translate('core.ui.primaryNavSection.exitWorkspaceLabel', { + defaultMessage: 'Exit workspace', + })} + onClick={exitWorkspace} + iconType={'exit'} /> - - - + )} + {/* Docking button only for larger screens that can support it*/} + { + + { + onIsLockedUpdate(!isLocked); + if (lockRef.current) { + lockRef.current.focus(); + } + }} + iconType={isLocked ? 'lock' : 'lockOpen'} + /> + + } + + ); diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index cd969fcc7275..08d050575385 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -70,6 +70,8 @@ function mockProps() { isLocked$: new BehaviorSubject(false), loadingCount$: new BehaviorSubject(0), onIsLockedUpdate: () => {}, + exitWorkspace: () => {}, + getWorkspaceUrl: (id: string) => '', branding: { darkMode: false, logo: { defaultUrl: '/' }, @@ -78,6 +80,7 @@ function mockProps() { }, survey: '/', currentWorkspace$: workspacesServiceMock.createStartContract().client.currentWorkspace$, + workspaceList$: workspacesServiceMock.createStartContract().client.workspaceList$, }; } diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index a2b218ae4087..c0fc9622fe5d 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -89,9 +89,12 @@ export interface HeaderProps { isLocked$: Observable; loadingCount$: ReturnType; onIsLockedUpdate: OnIsLockedUpdate; + exitWorkspace: () => void; + getWorkspaceUrl: (id: string) => string; branding: ChromeBranding; survey: string | undefined; currentWorkspace$: BehaviorSubject; + workspaceList$: BehaviorSubject; } export function Header({ @@ -100,6 +103,8 @@ export function Header({ application, basePath, onIsLockedUpdate, + exitWorkspace, + getWorkspaceUrl, homeHref, branding, survey, @@ -249,6 +254,8 @@ export function Header({ navigateToApp={application.navigateToApp} navigateToUrl={application.navigateToUrl} onIsLockedUpdate={onIsLockedUpdate} + exitWorkspace={exitWorkspace} + getWorkspaceUrl={getWorkspaceUrl} closeNav={() => { setIsNavOpen(false); if (toggleCollapsibleNavRef.current) { @@ -258,6 +265,7 @@ export function Header({ customNavLink$={observables.customNavLink$} branding={branding} currentWorkspace$={observables.currentWorkspace$} + workspaceList$={observables.workspaceList$} /> diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 11ff0b472bd0..8281b1ee2f96 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -31,7 +31,12 @@ import { EuiIcon } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import React from 'react'; -import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; +import { + ChromeNavLink, + ChromeRecentlyAccessedHistoryItem, + CoreStart, + WorkspaceAttribute, +} from '../../..'; import { HttpStart } from '../../../http'; import { InternalApplicationStart } from '../../../application/types'; import { relativeToAbsolute } from '../../nav_links/to_nav_link'; @@ -148,3 +153,34 @@ export function createRecentNavLink( }, }; } + +export interface WorkspaceNavLink { + label: string; + title: string; + 'aria-label': string; +} + +export function createWorkspaceNavLink( + href: string, + workspace: WorkspaceAttribute, + navLinks: ChromeNavLink[] +): WorkspaceNavLink { + const label = workspace.name; + let titleAndAriaLabel = label; + const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl)); + if (navLink) { + titleAndAriaLabel = i18n.translate('core.ui.workspaceLinks.linkItem.screenReaderLabel', { + defaultMessage: '{workspaceItemLinkName}, type: {pageType}', + values: { + workspaceItemLinkName: label, + pageType: navLink.title, + }, + }); + } + + return { + label, + title: titleAndAriaLabel, + 'aria-label': titleAndAriaLabel, + }; +} diff --git a/src/plugins/workspace/public/components/workspace_app.tsx b/src/plugins/workspace/public/components/workspace_app.tsx index ae2720d75b30..ec31f511da96 100644 --- a/src/plugins/workspace/public/components/workspace_app.tsx +++ b/src/plugins/workspace/public/components/workspace_app.tsx @@ -5,11 +5,11 @@ import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; -import { Route, Switch, useLocation } from 'react-router-dom'; - +import { Route, Switch, Redirect, useLocation } from 'react-router-dom'; import { ROUTES } from './routes'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; import { createBreadcrumbsFromPath } from './utils/breadcrumbs'; +import { PATHS } from '../../common/constants'; export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { const { @@ -31,6 +31,7 @@ export const WorkspaceApp = ({ appBasePath }: { appBasePath: string }) => { {ROUTES.map(({ path, Component, exact }) => ( } exact={exact ?? false} /> ))} +
); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 4925015306f9..4933cda2a43a 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -112,12 +112,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { public start(core: CoreStart) { mountDropdownList(core); - - core.chrome.setCustomNavLink({ - title: i18n.translate('workspace.nav.title', { defaultMessage: 'Workspace Overview' }), - baseUrl: core.http.basePath.get(), - href: core.application.getUrlForApp(WORKSPACE_APP_ID, { path: PATHS.update }), - }); this._changeSavedObjectCurrentWorkspace(); return {}; } From dcc9869fc18940a0529b895c1c06bddce4123ad3 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 10 Jul 2023 10:12:01 +0800 Subject: [PATCH 48/54] feat: make url stateful (#35) * feat: make url stateful Signed-off-by: SuZhoue-Joe * feat: optimize code Signed-off-by: SuZhoue-Joe * feat: remove useless change Signed-off-by: SuZhoue-Joe * feat: optimize url listener Signed-off-by: SuZhoue-Joe * feat: make formatUrlWithWorkspaceId extensible Signed-off-by: SuZhoue-Joe * feat: modify to related components Signed-off-by: SuZhoue-Joe * feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe * feat: modify the async format to be sync function Signed-off-by: SuZhoue-Joe * fix: type check Signed-off-by: SuZhoue-Joe * feat: use path to maintain workspace info Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhoue-Joe Signed-off-by: SuZhou-Joe --- .../fatal_errors/fatal_errors_service.mock.ts | 1 + src/core/public/http/base_path.ts | 24 +++++-- src/core/public/http/http_service.ts | 3 +- src/core/public/http/types.ts | 11 +++- src/core/public/index.ts | 1 - .../injected_metadata_service.ts | 11 ++++ src/core/public/utils/index.ts | 1 + src/core/public/utils/workspace.ts | 15 +++++ src/core/public/workspace/consts.ts | 2 - src/core/public/workspace/index.ts | 1 - .../public/workspace/workspaces_service.ts | 30 +++------ .../server/workspaces/workspaces_service.ts | 19 ++++++ src/plugins/workspace/common/constants.ts | 1 - .../public/components/workspace_overview.tsx | 17 ----- .../workspace_updater/workspace_updater.tsx | 19 +++--- src/plugins/workspace/public/plugin.ts | 66 +++++++------------ 16 files changed, 118 insertions(+), 104 deletions(-) create mode 100644 src/core/public/utils/workspace.ts diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index e495d66ae568..a28547bf88ed 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -82,6 +82,7 @@ const createWorkspacesSetupContractMock = () => ({ update: jest.fn(), }, formatUrlWithWorkspaceId: jest.fn(), + setFormatUrlWithWorkspaceId: jest.fn(), }); const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index b31504676dba..8c45d707cf26 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -33,14 +33,28 @@ import { modifyUrl } from '@osd/std'; export class BasePath { constructor( private readonly basePath: string = '', - public readonly serverBasePath: string = basePath + public readonly serverBasePath: string = basePath, + private readonly workspaceBasePath: string = '' ) {} public get = () => { + return `${this.basePath}${this.workspaceBasePath}`; + }; + + public getBasePath = () => { return this.basePath; }; public prepend = (path: string): string => { + if (!this.get()) return path; + return modifyUrl(path, (parts) => { + if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { + parts.pathname = `${this.get()}${parts.pathname}`; + } + }); + }; + + public prependWithoutWorkspacePath = (path: string): string => { if (!this.basePath) return path; return modifyUrl(path, (parts) => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { @@ -50,16 +64,16 @@ export class BasePath { }; public remove = (path: string): string => { - if (!this.basePath) { + if (!this.get()) { return path; } - if (path === this.basePath) { + if (path === this.get()) { return '/'; } - if (path.startsWith(`${this.basePath}/`)) { - return path.slice(this.basePath.length); + if (path.startsWith(`${this.get()}/`)) { + return path.slice(this.get().length); } return path; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index f26323f261aa..10d51bb2de7d 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -52,7 +52,8 @@ export class HttpService implements CoreService { const opensearchDashboardsVersion = injectedMetadata.getOpenSearchDashboardsVersion(); const basePath = new BasePath( injectedMetadata.getBasePath(), - injectedMetadata.getServerBasePath() + injectedMetadata.getServerBasePath(), + injectedMetadata.getWorkspaceBasePath() ); const fetchService = new Fetch({ basePath, opensearchDashboardsVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index ab046e6d2d5a..e5fb68b464e9 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -93,17 +93,17 @@ export type HttpStart = HttpSetup; */ export interface IBasePath { /** - * Gets the `basePath` string. + * Gets the `basePath + workspace` string. */ get: () => string; /** - * Prepends `path` with the basePath. + * Prepends `path` with the basePath + workspace. */ prepend: (url: string) => string; /** - * Removes the prepended basePath from the `path`. + * Removes the prepended basePath + workspace from the `path`. */ remove: (url: string) => string; @@ -113,6 +113,11 @@ export interface IBasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ readonly serverBasePath: string; + + /** + * Prepends `path` with the basePath. + */ + prependWithoutWorkspacePath: (url: string) => string; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 236048a012e7..ae35430d5800 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -356,5 +356,4 @@ export { WorkspacesService, WorkspaceAttribute, WorkspaceFindOptions, - WORKSPACE_ID_QUERYSTRING_NAME, } from './workspace'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index f4c6a7f7b91a..ccda2fbc925a 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -38,6 +38,7 @@ import { UserProvidedValues, } from '../../server/types'; import { AppCategory, Branding } from '../'; +import { getWorkspaceIdFromUrl } from '../utils'; export interface InjectedPluginMetadata { id: PluginName; @@ -151,6 +152,15 @@ export class InjectedMetadataService { getSurvey: () => { return this.state.survey; }, + + getWorkspaceBasePath: () => { + const workspaceId = getWorkspaceIdFromUrl(window.location.href); + if (workspaceId) { + return `/w/${workspaceId}`; + } + + return ''; + }, }; } } @@ -186,6 +196,7 @@ export interface InjectedMetadataSetup { }; getBranding: () => Branding; getSurvey: () => string | undefined; + getWorkspaceBasePath: () => string; } /** @internal */ diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index 7676b9482aac..0719f5e83c53 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -31,3 +31,4 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; export { MountWrapper, mountReactNode } from './mount'; +export { getWorkspaceIdFromUrl } from './workspace'; diff --git a/src/core/public/utils/workspace.ts b/src/core/public/utils/workspace.ts new file mode 100644 index 000000000000..e93355aa00e3 --- /dev/null +++ b/src/core/public/utils/workspace.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const getWorkspaceIdFromUrl = (url: string): string => { + const regexp = /\/w\/([^\/]*)/; + const urlObject = new URL(url); + const matchedResult = urlObject.pathname.match(regexp); + if (matchedResult) { + return matchedResult[1]; + } + + return ''; +}; diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts index 662baeaa5d19..b02fa29f1013 100644 --- a/src/core/public/workspace/consts.ts +++ b/src/core/public/workspace/consts.ts @@ -5,8 +5,6 @@ export const WORKSPACES_API_BASE_URL = '/api/workspaces'; -export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; - export enum WORKSPACE_ERROR_REASON_MAP { WORKSPACE_STALED = 'WORKSPACE_STALED', } diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index d0fb17ead0c1..359eee93f664 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -5,4 +5,3 @@ export { WorkspacesClientContract, WorkspacesClient } from './workspaces_client'; export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; export type { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; -export { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 908530885760..7d30ac52f49f 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -5,7 +5,6 @@ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; import type { WorkspaceAttribute } from '../../server/types'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; import { HttpSetup } from '../http'; /** @@ -14,43 +13,34 @@ import { HttpSetup } from '../http'; export interface WorkspacesStart { client: WorkspacesClientContract; formatUrlWithWorkspaceId: (url: string, id: WorkspaceAttribute['id']) => string; + setFormatUrlWithWorkspaceId: (formatFn: WorkspacesStart['formatUrlWithWorkspaceId']) => void; } export type WorkspacesSetup = WorkspacesStart; -function setQuerystring(url: string, params: Record): string { - const urlObj = new URL(url); - const searchParams = new URLSearchParams(urlObj.search); - - for (const key in params) { - if (params.hasOwnProperty(key)) { - const value = params[key]; - searchParams.set(key, value); - } - } - - urlObj.search = searchParams.toString(); - return urlObj.toString(); -} - export class WorkspacesService implements CoreService { private client?: WorkspacesClientContract; private formatUrlWithWorkspaceId(url: string, id: string) { - return setQuerystring(url, { - [WORKSPACE_ID_QUERYSTRING_NAME]: id, - }); + return url; + } + private setFormatUrlWithWorkspaceId(formatFn: WorkspacesStart['formatUrlWithWorkspaceId']) { + this.formatUrlWithWorkspaceId = formatFn; } public async setup({ http }: { http: HttpSetup }) { this.client = new WorkspacesClient(http); return { client: this.client, - formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + formatUrlWithWorkspaceId: (url: string, id: string) => this.formatUrlWithWorkspaceId(url, id), + setFormatUrlWithWorkspaceId: (fn: WorkspacesStart['formatUrlWithWorkspaceId']) => + this.setFormatUrlWithWorkspaceId(fn), }; } public async start(): Promise { return { client: this.client as WorkspacesClientContract, formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + setFormatUrlWithWorkspaceId: (fn: WorkspacesStart['formatUrlWithWorkspaceId']) => + this.setFormatUrlWithWorkspaceId(fn), }; } public async stop() { diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 6faec9a6496e..7aa01db34beb 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -2,6 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { URL } from 'node:url'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { InternalHttpServiceSetup } from '../http'; @@ -43,11 +44,29 @@ export class WorkspacesService this.logger = coreContext.logger.get('workspaces-service'); } + private proxyWorkspaceTrafficToRealHandler(setupDeps: WorkspacesSetupDeps) { + /** + * Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to + * {basePath}{osdPath*} + */ + setupDeps.http.registerOnPreRouting((request, response, toolkit) => { + const regexp = /\/w\/([^\/]*)/; + const matchedResult = request.url.pathname.match(regexp); + if (matchedResult) { + const requestUrl = new URL(request.url.toString()); + requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); + return toolkit.rewriteUrl(requestUrl.toString()); + } + return toolkit.next(); + }); + } + public async setup(setupDeps: WorkspacesSetupDeps): Promise { this.logger.debug('Setting up Workspaces service'); this.client = new WorkspacesClientWithSavedObject(setupDeps); await this.client.setup(setupDeps); + this.proxyWorkspaceTrafficToRealHandler(setupDeps); registerRoutes({ http: setupDeps.http, diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 2f67640bcd3f..557b889d6111 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -5,7 +5,6 @@ export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; -export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; export const PATHS = { create: '/create', diff --git a/src/plugins/workspace/public/components/workspace_overview.tsx b/src/plugins/workspace/public/components/workspace_overview.tsx index 97c9a07092d9..55de87d20b66 100644 --- a/src/plugins/workspace/public/components/workspace_overview.tsx +++ b/src/plugins/workspace/public/components/workspace_overview.tsx @@ -7,11 +7,8 @@ import React, { useState } from 'react'; import { EuiPageHeader, EuiButton, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; -import { i18n } from '@osd/i18n'; import { ApplicationStart } from '../../../../core/public'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { PATHS } from '../../common/constants'; -import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../../common/constants'; export const WorkspaceOverview = () => { const { @@ -22,20 +19,6 @@ export const WorkspaceOverview = () => { workspaces ? workspaces.client.currentWorkspace$ : of(null) ); - const onUpdateWorkspaceClick = () => { - if (!currentWorkspace || !currentWorkspace.id) { - notifications?.toasts.addDanger({ - title: i18n.translate('Cannot find current workspace', { - defaultMessage: 'Cannot update workspace', - }), - }); - return; - } - application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.update + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, - }); - }; - return ( <> diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index f3c8be0abb48..a3dc973ee095 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -21,11 +21,7 @@ import { useOpenSearchDashboards } from '../../../../../../src/plugins/opensearc import { PATHS } from '../../../common/constants'; import { WorkspaceForm, WorkspaceFormData } from '../workspace_creator/workspace_form'; -import { - WORKSPACE_APP_ID, - WORKSPACE_ID_IN_SESSION_STORAGE, - WORKSPACE_OP_TYPE_UPDATE, -} from '../../../common/constants'; +import { WORKSPACE_APP_ID, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants'; import { ApplicationStart } from '../../../../../core/public'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; @@ -80,9 +76,14 @@ export const WorkspaceUpdater = () => { defaultMessage: 'Update workspace successfully', }), }); - await application.navigateToApp(WORKSPACE_APP_ID, { - path: PATHS.overview + '?' + WORKSPACE_ID_IN_SESSION_STORAGE + '=' + currentWorkspace.id, - }); + window.location.href = + workspaces?.formatUrlWithWorkspaceId( + application.getUrlForApp(WORKSPACE_APP_ID, { + path: PATHS.overview, + absolute: true, + }), + currentWorkspace.id + ) || ''; return; } notifications?.toasts.addDanger({ @@ -92,7 +93,7 @@ export const WorkspaceUpdater = () => { text: result?.error, }); }, - [notifications?.toasts, workspaces?.client, currentWorkspace, application] + [notifications?.toasts, workspaces, currentWorkspace, application] ); if (!currentWorkspaceFormData.name) { diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 4933cda2a43a..07f1b84f32fe 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -11,59 +11,42 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE, PATHS } from '../common/constants'; -import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; +import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; import { mountDropdownList } from './mount'; +import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; export class WorkspacesPlugin implements Plugin<{}, {}> { private core?: CoreSetup; - private addWorkspaceListener() { - this.core?.workspaces.client.currentWorkspaceId$.subscribe((newWorkspaceId) => { - try { - sessionStorage.setItem(WORKSPACE_ID_IN_SESSION_STORAGE, newWorkspaceId); - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ - } - }); + private getWorkpsaceIdFromURL(): string | null { + return getWorkspaceIdFromUrl(window.location.href); } - private getWorkpsaceIdFromQueryString(): string | null { - const searchParams = new URLSearchParams(window.location.search); - return searchParams.get(WORKSPACE_ID_QUERYSTRING_NAME); - } - private getWorkpsaceIdFromSessionStorage(): string { - try { - return sessionStorage.getItem(WORKSPACE_ID_IN_SESSION_STORAGE) || ''; - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ - return ''; - } - } - private clearWorkspaceIdFromSessionStorage(): void { - try { - sessionStorage.removeItem(WORKSPACE_ID_IN_SESSION_STORAGE); - } catch (e) { - /** - * in incognize mode, this method may throw an error - * */ + private getPatchedUrl = (url: string, workspaceId: string) => { + const newUrl = new URL(url, window.location.href); + /** + * Patch workspace id into path + */ + newUrl.pathname = this.core?.http.basePath.remove(newUrl.pathname) || ''; + if (workspaceId) { + newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ + newUrl.pathname + }`; + } else { + newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; } - } + + return newUrl.toString(); + }; public async setup(core: CoreSetup) { this.core = core; + this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); /** - * Retrive workspace id from url or sessionstorage - * url > sessionstorage + * Retrive workspace id from url */ - const workspaceId = - this.getWorkpsaceIdFromQueryString() || this.getWorkpsaceIdFromSessionStorage(); + const workspaceId = this.getWorkpsaceIdFromURL(); if (workspaceId) { const result = await core.workspaces.client.enterWorkspace(workspaceId); if (!result.success) { - this.clearWorkspaceIdFromSessionStorage(); core.fatalErrors.add( result.error || i18n.translate('workspace.error.setup', { @@ -73,11 +56,6 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { } } - /** - * register a listener - */ - this.addWorkspaceListener(); - core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', { From e88f967a6532da506b618036ce33b871afacc600 Mon Sep 17 00:00:00 2001 From: raintygao Date: Tue, 11 Jul 2023 15:10:03 +0800 Subject: [PATCH 49/54] Fix build error and part of test error (#42) * fix: fix build error and some ut Signed-off-by: tygao * chore: remove saved object client test diff Signed-off-by: tygao --------- Signed-off-by: tygao --- .../collapsible_nav.test.tsx.snap | 7668 +++++++++-------- .../header/__snapshots__/header.test.tsx.snap | 1199 +-- .../chrome/ui/header/collapsible_nav.test.tsx | 2 +- .../injected_metadata_service.mock.ts | 1 + .../get_sorted_objects_for_export.test.ts | 6 + .../build_active_mappings.test.ts.snap | 8 + .../migrations/core/index_migrator.test.ts | 12 + ...pensearch_dashboards_migrator.test.ts.snap | 4 + .../dashboard_empty_screen.test.tsx.snap | 9 + .../saved_objects_table.test.tsx.snap | 9 + .../__snapshots__/flyout.test.tsx.snap | 3 + .../__snapshots__/header.test.tsx.snap | 8 +- ...telemetry_management_section.test.tsx.snap | 3 + 13 files changed, 4659 insertions(+), 4273 deletions(-) diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 382f88a562ce..2cb90eef6b9d 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -55,9 +55,12 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -171,48 +174,12 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "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, - }, - ], + "observers": Array [], "thrownError": null, } } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} homeHref="/" id="collapsibe-nav" isLocked={false} @@ -224,9 +191,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` Object { "baseUrl": "/", "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", + "id": "library", + "label": "Library", "order": 1000, }, "data-test-subj": "discover", @@ -280,9 +246,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` Object { "baseUrl": "/", "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", + "id": "library", + "label": "Library", "order": 1000, }, "data-test-subj": "visualize", @@ -294,9 +259,8 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` Object { "baseUrl": "/", "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", + "id": "library", + "label": "Library", "order": 1000, }, "data-test-subj": "dashboard", @@ -395,6 +359,25 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "closed": false, "hasError": false, "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, "observers": Array [ Subscriber { "_parentOrParents": null, @@ -433,18 +416,47 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "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, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } >
- -
+ + +
+ +

- Custom link - - - - - - + Home +

+ +
+ +
+
- -
- -
-
- - - - -

- Recently viewed -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
- -
-
- -
+
-
- -
+ + +
+ +

-
  • - - - recent 2 - - -
  • - - - -

    + Alerts + + +
    +
    -
    - + +
    - -
    -
    - -
    -
    - -
    + @@ -877,27 +650,21 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -964,7 +731,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -991,145 +758,68 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
    - -
    + +
    + + +
    + -
  • - - - dashboard - - -
  • - - - +

    + SEE MORE +

    +
    + +
    + @@ -1139,14 +829,10 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
    @@ -1173,27 +859,21 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -1260,7 +940,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -1287,106 +967,70 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
    - -
    + +
    + + +
    + -
  • - - - logs - - -
  • - - - +

    + SEE MORE +

    +
    + + +
    @@ -1396,555 +1040,127 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` - +
    - - - - - -

    - Security -

    -
    -
    -
    - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoSecurity" - data-test-subj="collapsibleNavGroup-securitySolution" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
    -
    - -
    -
    - -
    + + +
    + +
    -
    - - - -
    + Admin + +
    -
    - -
    + +
    + -
    +
    - - - - - - - -

    - Management -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="managementApp" - data-test-subj="collapsibleNavGroup-management" + +
    -
    - , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" > -
    - -
    - - - -
    -
    - -
    - -

    - Management -

    -
    -
    -
    -
    - - - -
    -
    - -
    -
    -
    - - - -
    -
    -
    -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    - - -
    -
    - -
      - - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
    • -
    • -
    -
    -
    + + +
    -
    -
    +
    +
    @@ -2035,9 +1251,12 @@ exports[`CollapsibleNav renders the default nav 1`] = ` BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -2106,48 +1325,12 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "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, - }, - ], + "observers": Array [], "thrownError": null, } } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} homeHref="/" id="collapsibe-nav" isLocked={false} @@ -2205,6 +1388,25 @@ exports[`CollapsibleNav renders the default nav 1`] = ` navigateToUrl={[Function]} onIsLockedUpdate={[Function]} recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -2253,14 +1455,6 @@ exports[`CollapsibleNav renders the default nav 1`] = ` "thrownError": null, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } > + + +`; + +exports[`CollapsibleNav renders the default nav 3`] = ` + - - -`; - -exports[`CollapsibleNav renders the default nav 3`] = ` - + + + + + + +`; + +exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = ` + - - - - - - -`; - -exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = ` - - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    - + Home + + +
    + +
    + +
    +
    +
    - -
    +
    -
    - -
      - + +
    + + +
    + +

    -
  • - - - recent - - -
  • - - - -

    + Alerts + + +
    +
    -
    - + + - -
    -
    - -
    -
    - -
    + @@ -3936,27 +3591,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/darkModeLogo" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -4023,7 +3672,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -4050,67 +3699,68 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] =
    - +
    + +
    +

    + No Favorites +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    +
    @@ -4120,14 +3770,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] =
    @@ -4154,27 +3800,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -4241,7 +3881,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -4268,67 +3908,70 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] =
    - +
    + +
    +

    + No Workspaces +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    + @@ -4337,27 +3980,86 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 1`] = - - +
    -
    +
    + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    + +
    +
    + + +
    +
    + - -
      -
    -
    -
    + + +
    -
    - +
    +
    @@ -4490,9 +4192,12 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -4734,18 +4439,251 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - customNavLink$={ - BehaviorSubject { - "_isScalar": false, - "_value": undefined, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ + ], + "thrownError": null, + } + } + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} + homeHref="/" + id="collapsibe-nav" + isLocked={false} + isNavOpen={true} + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "/", + "category": Object { + "id": "library", + "label": "Library", + "order": 1000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + Object { + "baseUrl": "/", + "category": Object { + "euiIconType": "logoObservability", + "id": "observability", + "label": "Observability", + "order": 3000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + ], + "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, + } + } + navigateToApp={[Function]} + navigateToUrl={[Function]} + onIsLockedUpdate={[Function]} + recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "id": "recent", + "label": "recent", + "link": "recent", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "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, + }, + 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 [ @@ -4783,51 +4721,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -4865,27 +4758,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[Function]} - navigateToUrl={[Function]} - onIsLockedUpdate={[Function]} - recentlyAccessed$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "id": "recent", - "label": "recent", - "link": "recent", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -4927,14 +4799,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = "thrownError": null, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } > - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    - + Home + + +
    + +
    + +
    +
    +
    - -
    +
    -
    - - - -
    + + +
    + + +
    + +

    + Alerts +

    +
    +
    +
    -
    - + + - - -
    - -
    -
    - -
    + @@ -5215,27 +4992,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -5302,7 +5073,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -5329,67 +5100,68 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] =
    - +
    + +
    +

    + No Favorites +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    +
    @@ -5399,14 +5171,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] =
    @@ -5433,27 +5201,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -5520,7 +5282,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -5547,67 +5309,70 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] =
    - +
    + +
    +

    + No Workspaces +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    + @@ -5616,27 +5381,86 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 2`] = - - +
    -
    - + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      -
        -
      - -
    + + +
    -
    -
    + + @@ -5761,34 +5585,185 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - basePath={ - BasePath { - "basePath": "/test", - "get": [Function], - "prepend": [Function], - "remove": [Function], - "serverBasePath": "/test", - } - } - branding={ - Object { - "darkMode": false, - "mark": Object {}, - } - } - closeNav={[Function]} - currentWorkspace$={ - BehaviorSubject { - "_isScalar": false, - "_value": null, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ + ], + "thrownError": null, + } + } + basePath={ + BasePath { + "basePath": "/test", + "get": [Function], + "getBasePath": [Function], + "prepend": [Function], + "prependWithoutWorkspacePath": [Function], + "remove": [Function], + "serverBasePath": "/test", + "workspaceBasePath": "", + } + } + branding={ + Object { + "darkMode": false, + "mark": Object {}, + } + } + closeNav={[Function]} + currentWorkspace$={ + BehaviorSubject { + "_isScalar": false, + "_value": null, + "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, + }, + 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, + }, Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -5863,6 +5838,63 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} + homeHref="/" + id="collapsibe-nav" + isLocked={false} + isNavOpen={true} + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "/", + "category": Object { + "id": "library", + "label": "Library", + "order": 1000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + Object { + "baseUrl": "/", + "category": Object { + "euiIconType": "logoObservability", + "id": "observability", + "label": "Observability", + "order": 3000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -5900,6 +5932,46 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navigateToApp={[Function]} + navigateToUrl={[Function]} + onIsLockedUpdate={[Function]} + recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "id": "recent", + "label": "recent", + "link": "recent", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -6011,18 +6083,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - customNavLink$={ - BehaviorSubject { - "_isScalar": false, - "_value": undefined, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -6060,51 +6120,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -6142,27 +6157,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[Function]} - navigateToUrl={[Function]} - onIsLockedUpdate={[Function]} - recentlyAccessed$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "id": "recent", - "label": "recent", - "link": "recent", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -6204,14 +6198,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = "thrownError": null, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } > - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    - + Home + + +
    + +
    + +
    +
    +
    - -
    +
    -
    - -
      - + +
    + + +
    + +

    -
  • - - - recent - - -
  • - - - -

    + Alerts + + +
    +
    -
    - + + - - -
    - -
    -
    - -
    + @@ -6492,27 +6391,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="undefined/opensearch_mark_default_mode.svg" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -6579,7 +6472,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -6606,67 +6499,68 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] =
    - +
    + +
    +

    + No Favorites +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    +
    @@ -6676,14 +6570,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] =
    @@ -6710,27 +6600,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -6797,7 +6681,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -6824,67 +6708,70 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] =
    - +
    + +
    +

    + No Workspaces +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    + @@ -6893,27 +6780,86 @@ exports[`CollapsibleNav renders the nav bar with custom logo in dark mode 3`] = - - +
    -
    +
    + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    + +
    +
    + + +
    +
    + - -
      -
    -
    -
    + + +
    -
    - +
    +
    @@ -7046,9 +6992,12 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -7138,11 +7087,179 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "syncErrorThrown": false, "syncErrorValue": null, }, - "isStopped": false, - "syncErrorThrowable": true, - "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, + }, + 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, + } + } + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} + homeHref="/" + id="collapsibe-nav" + isLocked={false} + isNavOpen={true} + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "/", + "category": Object { + "id": "library", + "label": "Library", + "order": 1000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + Object { + "baseUrl": "/", + "category": Object { + "euiIconType": "logoObservability", + "id": "observability", + "label": "Observability", + "order": 3000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -7180,6 +7297,46 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navigateToApp={[Function]} + navigateToUrl={[Function]} + onIsLockedUpdate={[Function]} + recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "id": "recent", + "label": "recent", + "link": "recent", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -7254,18 +7411,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - customNavLink$={ - BehaviorSubject { - "_isScalar": false, - "_value": undefined, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -7303,51 +7448,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -7385,27 +7485,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[Function]} - navigateToUrl={[Function]} - onIsLockedUpdate={[Function]} - recentlyAccessed$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "id": "recent", - "label": "recent", - "link": "recent", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -7447,14 +7526,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] "thrownError": null, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } > - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    - + Home + + +
    + +
    + +
    +
    +
    - -
    +
    -
    - -
      - + +
    + + +
    + +

    -
  • - - - recent - - -
  • - - - -

    + Alerts + + +
    +
    -
    - + + - - -
    - -
    -
    - -
    + @@ -7735,27 +7719,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="/defaultModeLogo" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -7822,7 +7800,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -7849,67 +7827,68 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`]
    - +
    + +
    +

    + No Favorites +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    +
    @@ -7919,14 +7898,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`]
    @@ -7953,27 +7928,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -8040,7 +8009,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -8067,67 +8036,70 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`]
    - +
    + +
    +

    + No Workspaces +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    + @@ -8136,27 +8108,86 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 1`] - - +
    -
    +
    + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    + +
    +
    + + +
    +
    + - -
      -
    -
    -
    + + +
    -
    - +
    +
    @@ -8289,9 +8320,12 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -8494,18 +8528,214 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - customNavLink$={ - BehaviorSubject { - "_isScalar": false, - "_value": undefined, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ + ], + "thrownError": null, + } + } + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": undefined, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + exitWorkspace={[Function]} + getWorkspaceUrl={[Function]} + homeHref="/" + id="collapsibe-nav" + isLocked={false} + isNavOpen={true} + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "/", + "category": Object { + "id": "library", + "label": "Library", + "order": 1000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + Object { + "baseUrl": "/", + "category": Object { + "euiIconType": "logoObservability", + "id": "observability", + "label": "Observability", + "order": 3000, + }, + "data-test-subj": "discover", + "href": "discover", + "id": "discover", + "isActive": true, + "title": "discover", + }, + ], + "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, + } + } + navigateToApp={[Function]} + navigateToUrl={[Function]} + onIsLockedUpdate={[Function]} + recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "id": "recent", + "label": "recent", + "link": "recent", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + storage={ + StubBrowserStorage { + "keys": Array [], + "size": 0, + "sizeLimit": 5000000, + "values": Array [], + } + } + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "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, + }, Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -8543,51 +8773,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - homeHref="/" - id="collapsibe-nav" - isLocked={false} - isNavOpen={true} - navLinks$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "inputOutput", - "id": "opensearchDashboards", - "label": "OpenSearch Dashboards", - "order": 1000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - Object { - "baseUrl": "/", - "category": Object { - "euiIconType": "logoObservability", - "id": "observability", - "label": "Observability", - "order": 3000, - }, - "data-test-subj": "discover", - "href": "discover", - "id": "discover", - "isActive": true, - "title": "discover", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -8625,27 +8810,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[Function]} - navigateToUrl={[Function]} - onIsLockedUpdate={[Function]} - recentlyAccessed$={ - BehaviorSubject { - "_isScalar": false, - "_value": Array [ - Object { - "id": "recent", - "label": "recent", - "link": "recent", - }, - ], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -8687,14 +8851,6 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] "thrownError": null, } } - storage={ - StubBrowserStorage { - "keys": Array [], - "size": 0, - "sizeLimit": 5000000, - "values": Array [], - } - } > - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" +
    -
    - + Home + + +
    + +
    + +
    +
    +
    - -
    +
    -
    - -
      - + +
    + + +
    + +

    -
  • - - - recent - - -
  • - - - -

    + Alerts + + +
    +
    -
    - + + - - -
    - -
    -
    - -
    + @@ -8975,27 +9044,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] className="euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="undefined/opensearch_mark_default_mode.svg" - data-test-subj="collapsibleNavGroup-opensearchDashboards" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -9062,7 +9125,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - OpenSearch Dashboards + Favorites
    @@ -9089,67 +9152,68 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`]
    - +
    + +
    +

    + No Favorites +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    +
    @@ -9159,14 +9223,10 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`]
    @@ -9193,27 +9253,21 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] className="euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces } className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="logoObservability" - data-test-subj="collapsibleNavGroup-observability" id="mockId" initialIsOpen={true} isLoading={false} isLoadingMessage={false} - onToggle={[Function]} paddingSize="none" >
    @@ -9280,7 +9334,7 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" id="mockId__title" > - Observability + Workspaces
    @@ -9307,67 +9361,70 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`]
    - +
    + +
    +

    + No Workspaces +

    +
    +
    +
    + + - -
    +

    + SEE MORE +

    +
    + +
    + @@ -9376,27 +9433,86 @@ exports[`CollapsibleNav renders the nav bar with custom logo in default mode 2`] - - +
    -
    +
    + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    + +
    +
    + + +
    +
    + - -
      -
    -
    -
    + + +
    -
    - +
    +
    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 5ee36fc58662..afee386a8b40 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 @@ -242,9 +242,12 @@ exports[`Header handles visibility and lock changes 1`] = ` BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -373,48 +376,11 @@ exports[`Header handles visibility and lock changes 1`] = ` "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, - }, - ], + "observers": Array [], "thrownError": null, } } + exitWorkspace={[Function]} forceAppSwitcherNavigation$={ BehaviorSubject { "_isScalar": false, @@ -501,6 +467,7 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } + getWorkspaceUrl={[Function]} helpExtension$={ BehaviorSubject { "_isScalar": false, @@ -1877,6 +1844,18 @@ exports[`Header handles visibility and lock changes 1`] = ` "closed": false, "hasError": false, "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + survey="/" + workspaceList$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, "observers": Array [ Subscriber { "_parentOrParents": null, @@ -1919,7 +1898,6 @@ exports[`Header handles visibility and lock changes 1`] = ` "thrownError": null, } } - survey="/" >
    - -
      - -
    • -
    + + +
    + +

    - Manage cloud deployment - - - - - - + Home +

    +
    +
    +
    +
    +
    - -
    - -
    -
    - - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
    - + Alerts + + +
    + +
    + + -
    + + + + + + + +

    + Favorites +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" id="mockId" - role="region" - tabIndex={-1} + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" > - -
    -
    +
    + Favorites + + +
    + +
    + + + - - - -
    -
    - -
    -
    - -
    - -
    -
    - -
      - -
    • +
      - -
    • -
      -
    -
    -
    -
    -
    - + +
    +

    + No Favorites +

    +
    +
    +
    + + +
    + +
    +

    + SEE MORE +

    +
    +
    +
    +
    + + + + + + + + + - + + + + + + +

    + Workspaces +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + id="mockId" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + paddingSize="none" + >
    - -
      + + + + + + +
      + +
      + + + +
      +
      + +
      + +

      + Workspaces +

      +
      +
      +
      +
      +
      +
      + +
    +
    + +
    +
    +
    + +
    + +
    +

    + No Workspaces +

    +
    +
    +
    +
    + +
    + +
    +

    + SEE MORE +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    + + + +
    +
    + +
    + +

    + Admin +

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
      + -
    -
    -
    + + +
    -
    - + +
    @@ -6701,9 +6892,12 @@ exports[`Header renders condensed header 1`] = ` BasePath { "basePath": "/test", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "/test", + "workspaceBasePath": "", } } branding={ @@ -6861,48 +7055,11 @@ exports[`Header renders condensed header 1`] = ` "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, - }, - ], + "observers": Array [], "thrownError": null, } } + exitWorkspace={[Function]} forceAppSwitcherNavigation$={ BehaviorSubject { "_isScalar": false, @@ -6952,6 +7109,7 @@ exports[`Header renders condensed header 1`] = ` "thrownError": null, } } + getWorkspaceUrl={[Function]} helpExtension$={ BehaviorSubject { "_isScalar": false, @@ -8196,6 +8354,18 @@ exports[`Header renders condensed header 1`] = ` opensearchDashboardsDocLink="/docs" opensearchDashboardsVersion="1.0.0" recentlyAccessed$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + } + } + survey="/" + workspaceList$={ BehaviorSubject { "_isScalar": false, "_value": Array [], @@ -8240,11 +8410,47 @@ exports[`Header renders condensed header 1`] = ` "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, } } - survey="/" >
    { recentlyAccessed$={new BehaviorSubject(recentNavLinks)} /> ); - expectShownNavLinksCount(component, 3); + expectShownNavLinksCount(component, 0); clickGroup(component, 'opensearchDashboards'); clickGroup(component, 'recentlyViewed'); expectShownNavLinksCount(component, 1); diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index c91a4e446b1e..14936a80b692 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -46,6 +46,7 @@ const createSetupContractMock = () => { getOpenSearchDashboardsBuildNumber: jest.fn(), getBranding: jest.fn(), getSurvey: jest.fn(), + getWorkspaceBasePath: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getOpenSearchDashboardsVersion.mockReturnValue('opensearchDashboardsVersion'); diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index cf7e1d8246a7..952a74a76940 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -128,6 +128,7 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], + workspaces: undefined, }, ], ], @@ -218,6 +219,7 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], + workspaces: undefined, }, ], ], @@ -368,6 +370,7 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], + workspaces: undefined, }, ], ], @@ -459,6 +462,7 @@ describe('getSortedObjectsForExport()', () => { index-pattern, search, ], + workspaces: undefined, }, ], ], @@ -666,6 +670,7 @@ describe('getSortedObjectsForExport()', () => { ], Object { namespace: undefined, + workspaces: undefined, }, ], ], @@ -784,6 +789,7 @@ describe('getSortedObjectsForExport()', () => { ], Object { namespace: undefined, + workspaces: undefined, }, ], Array [ diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index f8ef47cae894..254bdbb4b2c6 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -13,6 +13,7 @@ Object { "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "workspaces": "2f4316de49999235636386fe51dc06c1", }, }, "dynamic": "strict", @@ -56,6 +57,9 @@ Object { "updated_at": Object { "type": "date", }, + "workspaces": Object { + "type": "keyword", + }, }, } `; @@ -74,6 +78,7 @@ Object { "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "workspaces": "2f4316de49999235636386fe51dc06c1", }, }, "dynamic": "strict", @@ -134,6 +139,9 @@ Object { "updated_at": Object { "type": "date", }, + "workspaces": Object { + "type": "keyword", + }, }, } `; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 4bacfda3bd5a..4b65da5250d6 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -82,6 +82,7 @@ describe('IndexMigrator', () => { references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', + workspaces: '2f4316de49999235636386fe51dc06c1', }, }, properties: { @@ -92,6 +93,9 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + workspaces: { + type: 'keyword', + }, references: { type: 'nested', properties: { @@ -199,6 +203,7 @@ describe('IndexMigrator', () => { references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', + workspaces: '2f4316de49999235636386fe51dc06c1', }, }, properties: { @@ -210,6 +215,9 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + workspaces: { + type: 'keyword', + }, references: { type: 'nested', properties: { @@ -260,6 +268,7 @@ describe('IndexMigrator', () => { references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', + workspaces: '2f4316de49999235636386fe51dc06c1', }, }, properties: { @@ -271,6 +280,9 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + workspaces: { + type: 'keyword', + }, references: { type: 'nested', properties: { diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap index baebb7848798..811d5f3594bf 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap @@ -13,6 +13,7 @@ Object { "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "workspaces": "2f4316de49999235636386fe51dc06c1", }, }, "dynamic": "strict", @@ -64,6 +65,9 @@ Object { "updated_at": Object { "type": "date", }, + "workspaces": Object { + "type": "keyword", + }, }, } `; diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 04120e429393..27877953ea5f 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -12,9 +12,12 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` "basePath": BasePath { "basePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -379,9 +382,12 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` "basePath": BasePath { "basePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], @@ -756,9 +762,12 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] "basePath": BasePath { "basePath": "", "get": [Function], + "getBasePath": [Function], "prepend": [Function], + "prependWithoutWorkspacePath": [Function], "remove": [Function], "serverBasePath": "", + "workspaceBasePath": "", }, "delete": [MockFunction], "fetch": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index d18762f4912f..2ab1064a7db3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -200,6 +200,12 @@ exports[`SavedObjectsTable export should allow the user to choose when exporting exports[`SavedObjectsTable should render normally 1`] = `
    -

    - -

    +

    Date: Tue, 11 Jul 2023 17:39:26 +0800 Subject: [PATCH 50/54] feat: optimize code (#40) Signed-off-by: SuZhou-Joe --- src/core/public/http/base_path.ts | 16 ++++------------ src/core/public/http/http_service.ts | 8 +++++++- src/core/public/http/types.ts | 5 ----- .../injected_metadata_service.ts | 9 --------- src/plugins/workspace/public/plugin.ts | 8 ++++---- 5 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts index 8c45d707cf26..44d2560bb4c8 100644 --- a/src/core/public/http/base_path.ts +++ b/src/core/public/http/base_path.ts @@ -45,20 +45,12 @@ export class BasePath { return this.basePath; }; - public prepend = (path: string): string => { - if (!this.get()) return path; + public prepend = (path: string, withoutWorkspace: boolean = false): string => { + const basePath = withoutWorkspace ? this.basePath : this.get(); + if (!basePath) return path; return modifyUrl(path, (parts) => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { - parts.pathname = `${this.get()}${parts.pathname}`; - } - }); - }; - - public prependWithoutWorkspacePath = (path: string): string => { - if (!this.basePath) return path; - return modifyUrl(path, (parts) => { - if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { - parts.pathname = `${this.basePath}${parts.pathname}`; + parts.pathname = `${basePath}${parts.pathname}`; } }); }; diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 10d51bb2de7d..45f69f1a6926 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -36,6 +36,7 @@ import { AnonymousPathsService } from './anonymous_paths_service'; import { LoadingCountService } from './loading_count_service'; import { Fetch } from './fetch'; import { CoreService } from '../../types'; +import { getWorkspaceIdFromUrl } from '../utils'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; @@ -50,10 +51,15 @@ export class HttpService implements CoreService { public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { const opensearchDashboardsVersion = injectedMetadata.getOpenSearchDashboardsVersion(); + let workspaceBasePath = ''; + const workspaceId = getWorkspaceIdFromUrl(window.location.href); + if (workspaceId) { + workspaceBasePath = `/w/${workspaceId}`; + } const basePath = new BasePath( injectedMetadata.getBasePath(), injectedMetadata.getServerBasePath(), - injectedMetadata.getWorkspaceBasePath() + workspaceBasePath ); const fetchService = new Fetch({ basePath, opensearchDashboardsVersion }); const loadingCount = this.loadingCount.setup({ fatalErrors }); diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index e5fb68b464e9..9a466a1519c7 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -113,11 +113,6 @@ export interface IBasePath { * See {@link BasePath.get} for getting the basePath value for a specific request */ readonly serverBasePath: string; - - /** - * Prepends `path` with the basePath. - */ - prependWithoutWorkspacePath: (url: string) => string; } /** diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index ccda2fbc925a..88ea56cfa62e 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -152,15 +152,6 @@ export class InjectedMetadataService { getSurvey: () => { return this.state.survey; }, - - getWorkspaceBasePath: () => { - const workspaceId = getWorkspaceIdFromUrl(window.location.href); - if (workspaceId) { - return `/w/${workspaceId}`; - } - - return ''; - }, }; } } diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 07f1b84f32fe..31d996f3b341 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -11,13 +11,13 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID, PATHS } from '../common/constants'; +import { WORKSPACE_APP_ID } from '../common/constants'; import { mountDropdownList } from './mount'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; export class WorkspacesPlugin implements Plugin<{}, {}> { private core?: CoreSetup; - private getWorkpsaceIdFromURL(): string | null { + private getWorkspaceIdFromURL(): string | null { return getWorkspaceIdFromUrl(window.location.href); } private getPatchedUrl = (url: string, workspaceId: string) => { @@ -40,9 +40,9 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { this.core = core; this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); /** - * Retrive workspace id from url + * Retrieve workspace id from url */ - const workspaceId = this.getWorkpsaceIdFromURL(); + const workspaceId = this.getWorkspaceIdFromURL(); if (workspaceId) { const result = await core.workspaces.client.enterWorkspace(workspaceId); From bfc5c8aec2e496f09a4d1a81e8728632791c2c2a Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Wed, 12 Jul 2023 10:45:17 +0800 Subject: [PATCH 51/54] fix: bootstrap error (#43) Signed-off-by: SuZhou-Joe --- .../public/injected_metadata/injected_metadata_service.mock.ts | 1 - src/core/public/injected_metadata/injected_metadata_service.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 14936a80b692..c91a4e446b1e 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -46,7 +46,6 @@ const createSetupContractMock = () => { getOpenSearchDashboardsBuildNumber: jest.fn(), getBranding: jest.fn(), getSurvey: jest.fn(), - getWorkspaceBasePath: jest.fn(), }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getOpenSearchDashboardsVersion.mockReturnValue('opensearchDashboardsVersion'); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 88ea56cfa62e..f4c6a7f7b91a 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -38,7 +38,6 @@ import { UserProvidedValues, } from '../../server/types'; import { AppCategory, Branding } from '../'; -import { getWorkspaceIdFromUrl } from '../utils'; export interface InjectedPluginMetadata { id: PluginName; @@ -187,7 +186,6 @@ export interface InjectedMetadataSetup { }; getBranding: () => Branding; getSurvey: () => string | undefined; - getWorkspaceBasePath: () => string; } /** @internal */ From eb200a737d3045d207133299eb4effbfa12578b4 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 13 Jul 2023 09:28:46 +0800 Subject: [PATCH 52/54] feat: add workspace permission control interface (#41) * feat: add workspace permission control interface Signed-off-by: Lin Wang * feat: add request parameter for workspace permission control Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang --- src/core/server/workspaces/index.ts | 2 ++ .../workspace_permission_control.ts | 23 +++++++++++++++++++ .../server/workspaces/workspaces_service.ts | 11 +++++++++ 3 files changed, 36 insertions(+) create mode 100644 src/core/server/workspaces/workspace_permission_control.ts diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts index b9f765e4bba3..5441216c7314 100644 --- a/src/core/server/workspaces/index.ts +++ b/src/core/server/workspaces/index.ts @@ -11,3 +11,5 @@ export { } from './workspaces_service'; export { WorkspaceAttribute, WorkspaceFindOptions } from './types'; + +export { WorkspacePermissionControl } from './workspace_permission_control'; diff --git a/src/core/server/workspaces/workspace_permission_control.ts b/src/core/server/workspaces/workspace_permission_control.ts new file mode 100644 index 000000000000..bf85562c4669 --- /dev/null +++ b/src/core/server/workspaces/workspace_permission_control.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchDashboardsRequest } from '../http'; + +export enum WorkspacePermissionMode { + Read, + Admin, +} + +export class WorkspacePermissionControl { + public async validate( + workspaceId: string, + permissionModeOrModes: WorkspacePermissionMode | WorkspacePermissionMode[], + request: OpenSearchDashboardsRequest + ) { + return true; + } + + public async setup() {} +} diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 7aa01db34beb..887cf46af86a 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -14,13 +14,16 @@ import { } from '../saved_objects'; import { IWorkspaceDBImpl } from './types'; import { WorkspacesClientWithSavedObject } from './workspaces_client'; +import { WorkspacePermissionControl } from './workspace_permission_control'; export interface WorkspacesServiceSetup { client: IWorkspaceDBImpl; + permissionControl: WorkspacePermissionControl; } export interface WorkspacesServiceStart { client: IWorkspaceDBImpl; + permissionControl: WorkspacePermissionControl; } export interface WorkspacesSetupDeps { @@ -40,6 +43,8 @@ export class WorkspacesService implements CoreService { private logger: Logger; private client?: IWorkspaceDBImpl; + private permissionControl?: WorkspacePermissionControl; + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); } @@ -65,7 +70,11 @@ export class WorkspacesService this.logger.debug('Setting up Workspaces service'); this.client = new WorkspacesClientWithSavedObject(setupDeps); + this.permissionControl = new WorkspacePermissionControl(); + await this.client.setup(setupDeps); + await this.permissionControl.setup(); + this.proxyWorkspaceTrafficToRealHandler(setupDeps); registerRoutes({ @@ -76,6 +85,7 @@ export class WorkspacesService return { client: this.client, + permissionControl: this.permissionControl, }; } @@ -84,6 +94,7 @@ export class WorkspacesService return { client: this.client as IWorkspaceDBImpl, + permissionControl: this.permissionControl as WorkspacePermissionControl, }; } From 878d8a30b8f6d049159129ec52142e24b7110606 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Fri, 14 Jul 2023 14:57:50 +0800 Subject: [PATCH 53/54] allow user to turn on/off workspace from advance settings (#46) return 404 if accessing a workspace path when workspace is disabled --------- Signed-off-by: Yulong Ruan --- src/core/public/chrome/chrome_service.tsx | 4 +- src/core/public/core_app/core_app.ts | 2 - src/core/public/core_system.ts | 6 +-- src/core/public/http/types.ts | 5 ++ .../ui_settings/ui_settings_service.mock.ts | 9 ++-- .../public/workspace/workspaces_client.ts | 8 +-- .../public/workspace/workspaces_service.ts | 9 +++- src/core/server/server.ts | 1 + .../server/ui_settings/settings/index.test.ts | 2 + src/core/server/ui_settings/settings/index.ts | 2 + .../server/ui_settings/settings/workspace.ts | 25 +++++++++ .../server/workspaces/workspaces_service.ts | 29 +++++++--- .../workspace_dropdown_list.tsx | 17 +++--- src/plugins/workspace/public/mount.tsx | 19 +++++-- src/plugins/workspace/public/plugin.ts | 53 +++++++++++++------ 15 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 src/core/server/ui_settings/settings/workspace.ts diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index ee1cb5363772..a0afe640ec64 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -182,7 +182,7 @@ export class ChromeService { }); const getWorkspaceUrl = (id: string) => { - return workspaces?.formatUrlWithWorkspaceId( + return workspaces.formatUrlWithWorkspaceId( application.getUrlForApp(WORKSPACE_APP_ID, { path: '/', absolute: true, @@ -194,7 +194,7 @@ export class ChromeService { const exitWorkspace = async () => { let result; try { - result = await workspaces?.client.exitWorkspace(); + result = await workspaces.client.exitWorkspace(); } catch (error) { notifications?.toasts.addDanger({ title: i18n.translate('workspace.exit.failed', { diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index c4d359d58dc1..fcbcc5de5655 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -42,14 +42,12 @@ import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; import { renderApp as renderStatusApp } from './status'; -import { WorkspacesSetup } from '../workspace'; interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; - workspaces: WorkspacesSetup; } interface StartDeps { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 9512560112f7..1e756ddcf8c9 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -163,14 +163,14 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - const workspaces = await this.workspaces.setup({ http }); + const workspaces = await this.workspaces.setup({ http, uiSettings }); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies: new Map([...pluginDependencies]), }); const application = this.application.setup({ context, http }); - this.coreApp.setup({ application, http, injectedMetadata, notifications, workspaces }); + this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { application, @@ -204,7 +204,6 @@ export class CoreSystem { const uiSettings = await this.uiSettings.start(); const docLinks = this.docLinks.start({ injectedMetadata }); const http = await this.http.start(); - const workspaces = await this.workspaces.start(); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const fatalErrors = await this.fatalErrors.start(); @@ -226,6 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); + const workspaces = await this.workspaces.start(); const chrome = await this.chrome.start({ application, docLinks, diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 9a466a1519c7..4c81dbdd7a5f 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -97,6 +97,11 @@ export interface IBasePath { */ get: () => string; + /** + * Gets the `basePath + */ + getBasePath: () => string; + /** * Prepends `path` with the basePath + workspace. */ diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 8458c86d6774..2d9cead9682b 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -33,7 +33,7 @@ import type { PublicMethodsOf } from '@osd/utility-types'; import { UiSettingsService } from './'; import { IUiSettingsClient } from './types'; -const createSetupContractMock = () => { +const createUiSettingsClientMock = () => { const setupContract: jest.Mocked = { getAll: jest.fn(), get: jest.fn(), @@ -66,12 +66,13 @@ const createMock = () => { stop: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.setup.mockReturnValue(createUiSettingsClientMock()); + mocked.start.mockReturnValue(createUiSettingsClientMock()); return mocked; }; export const uiSettingsServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createSetupContractMock, + createSetupContract: createUiSettingsClientMock, + createStartContract: createUiSettingsClientMock, }; diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index f37fd89ae249..ac909b62deeb 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -70,10 +70,12 @@ export class WorkspacesClient { } } ); + } - /** - * Initialize workspace list - */ + /** + * Initialize workspace list + */ + init() { this.updateWorkspaceListAndNotify(); } diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index 7d30ac52f49f..cb82f3c44406 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -6,6 +6,7 @@ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; import type { WorkspaceAttribute } from '../../server/types'; import { HttpSetup } from '../http'; +import { IUiSettingsClient } from '../ui_settings'; /** * @public @@ -26,8 +27,14 @@ export class WorkspacesService implements CoreService this.formatUrlWithWorkspaceId(url, id), diff --git a/src/core/server/server.ts b/src/core/server/server.ts index f80b90ba6baa..dbaee6c12400 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -263,6 +263,7 @@ export class Server { }); await this.workspaces.start({ savedObjects: savedObjectsStart, + uiSettings: uiSettingsStart, }); this.coreStart = { diff --git a/src/core/server/ui_settings/settings/index.test.ts b/src/core/server/ui_settings/settings/index.test.ts index f71f852eb3ce..03564ce7e9b2 100644 --- a/src/core/server/ui_settings/settings/index.test.ts +++ b/src/core/server/ui_settings/settings/index.test.ts @@ -36,6 +36,7 @@ import { getNotificationsSettings } from './notifications'; import { getThemeSettings } from './theme'; import { getCoreSettings } from './index'; import { getStateSettings } from './state'; +import { getWorkspaceSettings } from './workspace'; describe('getCoreSettings', () => { it('should not have setting overlaps', () => { @@ -48,6 +49,7 @@ describe('getCoreSettings', () => { getNotificationsSettings(), getThemeSettings(), getStateSettings(), + getWorkspaceSettings(), ].reduce((sum, settings) => sum + Object.keys(settings).length, 0); expect(coreSettingsLength).toBe(summedLength); diff --git a/src/core/server/ui_settings/settings/index.ts b/src/core/server/ui_settings/settings/index.ts index b284744fc818..cea335117af8 100644 --- a/src/core/server/ui_settings/settings/index.ts +++ b/src/core/server/ui_settings/settings/index.ts @@ -36,6 +36,7 @@ import { getNavigationSettings } from './navigation'; import { getNotificationsSettings } from './notifications'; import { getThemeSettings } from './theme'; import { getStateSettings } from './state'; +import { getWorkspaceSettings } from './workspace'; export const getCoreSettings = (): Record => { return { @@ -46,5 +47,6 @@ export const getCoreSettings = (): Record => { ...getNotificationsSettings(), ...getThemeSettings(), ...getStateSettings(), + ...getWorkspaceSettings(), }; }; diff --git a/src/core/server/ui_settings/settings/workspace.ts b/src/core/server/ui_settings/settings/workspace.ts new file mode 100644 index 000000000000..3eb9e33b681c --- /dev/null +++ b/src/core/server/ui_settings/settings/workspace.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { i18n } from '@osd/i18n'; +import { UiSettingsParams } from '../../../types'; + +export const getWorkspaceSettings = (): Record => { + return { + 'workspace:enabled': { + name: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', { + defaultMessage: 'Enable Workspace', + }), + value: false, + requiresPageReload: true, + description: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', { + defaultMessage: 'Enable or disable OpenSearch Dashboards Workspace', + }), + category: ['workspace'], + schema: schema.boolean(), + }, + }; +}; diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 887cf46af86a..b25d1e9e1025 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -15,6 +15,7 @@ import { import { IWorkspaceDBImpl } from './types'; import { WorkspacesClientWithSavedObject } from './workspaces_client'; import { WorkspacePermissionControl } from './workspace_permission_control'; +import { UiSettingsServiceStart } from '../ui_settings/types'; export interface WorkspacesServiceSetup { client: IWorkspaceDBImpl; @@ -37,6 +38,7 @@ export type InternalWorkspacesServiceStart = WorkspacesServiceStart; /** @internal */ export interface WorkspacesStartDeps { savedObjects: InternalSavedObjectsServiceStart; + uiSettings: UiSettingsServiceStart; } export class WorkspacesService @@ -44,7 +46,7 @@ export class WorkspacesService private logger: Logger; private client?: IWorkspaceDBImpl; private permissionControl?: WorkspacePermissionControl; - + private startDeps?: WorkspacesStartDeps; constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); } @@ -52,15 +54,29 @@ export class WorkspacesService private proxyWorkspaceTrafficToRealHandler(setupDeps: WorkspacesSetupDeps) { /** * Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to - * {basePath}{osdPath*} + * {basePath}{osdPath*} when workspace is enabled + * + * Return HTTP 404 if accessing {basePath}/w/{workspaceId} when workspace is disabled */ - setupDeps.http.registerOnPreRouting((request, response, toolkit) => { + setupDeps.http.registerOnPreRouting(async (request, response, toolkit) => { const regexp = /\/w\/([^\/]*)/; const matchedResult = request.url.pathname.match(regexp); + if (matchedResult) { - const requestUrl = new URL(request.url.toString()); - requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); - return toolkit.rewriteUrl(requestUrl.toString()); + if (this.startDeps) { + const savedObjectsClient = this.startDeps.savedObjects.getScopedClient(request); + const uiSettingsClient = this.startDeps.uiSettings.asScopedToClient(savedObjectsClient); + const workspacesEnabled = await uiSettingsClient.get('workspace:enabled'); + + if (workspacesEnabled) { + const requestUrl = new URL(request.url.toString()); + requestUrl.pathname = requestUrl.pathname.replace(regexp, ''); + return toolkit.rewriteUrl(requestUrl.toString()); + } else { + // If workspace was disable, return HTTP 404 + return response.notFound(); + } + } } return toolkit.next(); }); @@ -90,6 +106,7 @@ export class WorkspacesService } public async start(deps: WorkspacesStartDeps): Promise { + this.startDeps = deps; this.logger.debug('Starting SavedObjects service'); return { diff --git a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx index 3dd50bb5886f..a53e39cf1647 100644 --- a/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx +++ b/src/plugins/workspace/public/containers/workspace_dropdown_list/workspace_dropdown_list.tsx @@ -7,14 +7,15 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; -import { CoreStart, WorkspaceAttribute } from '../../../../../core/public'; +import { ApplicationStart, WorkspaceAttribute, WorkspacesStart } from '../../../../../core/public'; import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants'; import { switchWorkspace } from '../../components/utils/workspace'; type WorkspaceOption = EuiComboBoxOptionOption; interface WorkspaceDropdownListProps { - coreStart: CoreStart; + workspaces: WorkspacesStart; + application: ApplicationStart; } function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { @@ -27,10 +28,8 @@ export function getErrorMessage(err: any) { } export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { - const { coreStart } = props; - - const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []); - const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null); + const workspaceList = useObservable(props.workspaces.client.workspaceList$, []); + const currentWorkspace = useObservable(props.workspaces.client.currentWorkspace$, null); const [loading, setLoading] = useState(false); const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]); @@ -58,14 +57,14 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) { /** switch the workspace */ setLoading(true); const id = workspaceOption[0].key!; - switchWorkspace(coreStart, id); + switchWorkspace({ workspaces: props.workspaces, application: props.application }, id); setLoading(false); }, - [coreStart] + [props.application, props.workspaces] ); const onCreateWorkspaceClick = () => { - coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); + props.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create }); }; useEffect(() => { diff --git a/src/plugins/workspace/public/mount.tsx b/src/plugins/workspace/public/mount.tsx index c4ca29479d23..dc2ff1de8c1e 100644 --- a/src/plugins/workspace/public/mount.tsx +++ b/src/plugins/workspace/public/mount.tsx @@ -5,14 +5,25 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart } from '../../../core/public'; +import { ApplicationStart, ChromeStart, WorkspacesStart } from '../../../core/public'; import { WorkspaceDropdownList } from './containers/workspace_dropdown_list'; -export const mountDropdownList = (core: CoreStart) => { - core.chrome.navControls.registerLeft({ +export const mountDropdownList = ({ + application, + workspaces, + chrome, +}: { + application: ApplicationStart; + workspaces: WorkspacesStart; + chrome: ChromeStart; +}) => { + chrome.navControls.registerLeft({ order: 0, mount: (element) => { - ReactDOM.render(, element); + ReactDOM.render( + , + element + ); return () => { ReactDOM.unmountComponentAtNode(element); }; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 31d996f3b341..c221e892aa58 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,9 +14,12 @@ import { import { WORKSPACE_APP_ID } from '../common/constants'; import { mountDropdownList } from './mount'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; +import type { Subscription } from 'rxjs'; export class WorkspacesPlugin implements Plugin<{}, {}> { - private core?: CoreSetup; + private coreSetup?: CoreSetup; + private coreStart?: CoreStart; + private currentWorkspaceSubscription?: Subscription; private getWorkspaceIdFromURL(): string | null { return getWorkspaceIdFromUrl(window.location.href); } @@ -25,20 +28,25 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { /** * Patch workspace id into path */ - newUrl.pathname = this.core?.http.basePath.remove(newUrl.pathname) || ''; + newUrl.pathname = this.coreSetup?.http.basePath.remove(newUrl.pathname) || ''; if (workspaceId) { - newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ + newUrl.pathname = `${this.coreSetup?.http.basePath.serverBasePath || ''}/w/${workspaceId}${ newUrl.pathname }`; } else { - newUrl.pathname = `${this.core?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; + newUrl.pathname = `${this.coreSetup?.http.basePath.serverBasePath || ''}${newUrl.pathname}`; } return newUrl.toString(); }; public async setup(core: CoreSetup) { - this.core = core; - this.core?.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); + // If workspace feature is disabled, it will not load the workspace plugin + if (core.uiSettings.get('workspace:enabled') === false) { + return {}; + } + + this.coreSetup = core; + core.workspaces.setFormatUrlWithWorkspaceId((url, id) => this.getPatchedUrl(url, id)); /** * Retrieve workspace id from url */ @@ -78,19 +86,34 @@ export class WorkspacesPlugin implements Plugin<{}, {}> { return {}; } - private async _changeSavedObjectCurrentWorkspace() { - const startServices = await this.core?.getStartServices(); - if (startServices) { - const coreStart = startServices[0]; - coreStart.workspaces.client.currentWorkspaceId$.subscribe((currentWorkspaceId) => { - coreStart.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); - }); + private _changeSavedObjectCurrentWorkspace() { + if (this.coreStart) { + return this.coreStart.workspaces.client.currentWorkspaceId$.subscribe( + (currentWorkspaceId) => { + this.coreStart?.savedObjects.client.setCurrentWorkspace(currentWorkspaceId); + } + ); } } public start(core: CoreStart) { - mountDropdownList(core); - this._changeSavedObjectCurrentWorkspace(); + // If workspace feature is disabled, it will not load the workspace plugin + if (core.uiSettings.get('workspace:enabled') === false) { + return {}; + } + + this.coreStart = core; + + mountDropdownList({ + application: core.application, + workspaces: core.workspaces, + chrome: core.chrome, + }); + this.currentWorkspaceSubscription = this._changeSavedObjectCurrentWorkspace(); return {}; } + + public stop() { + this.currentWorkspaceSubscription?.unsubscribe(); + } } From 0c83ccd20f4f7c95b9ebbda42027a855d4d1af2f Mon Sep 17 00:00:00 2001 From: gaobinlong Date: Thu, 13 Jul 2023 17:50:35 +0800 Subject: [PATCH 54/54] Add copy saved objects among workspaces functionality Signed-off-by: gaobinlong Signed-off-by: gaobinlong --- src/core/public/mocks.ts | 1 + src/core/server/saved_objects/routes/copy.ts | 82 +++++ src/core/server/saved_objects/routes/index.ts | 2 + .../public/constants.ts | 2 + .../public/lib/copy_saved_objects.ts | 27 ++ .../public/lib/index.ts | 1 + .../objects_table/components/copy_modal.tsx | 320 ++++++++++++++++++ .../objects_table/components/header.tsx | 18 + .../saved_objects_table.test.tsx | 4 + .../objects_table/saved_objects_table.tsx | 59 ++++ .../saved_objects_table_page.tsx | 1 + 11 files changed, 517 insertions(+) create mode 100644 src/core/server/saved_objects/routes/copy.ts create mode 100644 src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/copy_modal.tsx diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e863d627c801..5b0e1cd89dae 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -60,6 +60,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; +export { workspacesServiceMock } from './fatal_errors/fatal_errors_service.mock'; function createCoreSetupMock({ basePath = '', diff --git a/src/core/server/saved_objects/routes/copy.ts b/src/core/server/saved_objects/routes/copy.ts new file mode 100644 index 000000000000..27de8212d328 --- /dev/null +++ b/src/core/server/saved_objects/routes/copy.ts @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../http'; +import { SavedObjectConfig } from '../saved_objects_config'; +import { exportSavedObjectsToStream } from '../export'; +import { validateObjects } from './utils'; +import { importSavedObjectsFromStream } from '../import'; + +export const registerCopyRoute = (router: IRouter, config: SavedObjectConfig) => { + const { maxImportExportSize } = config; + + router.post( + { + path: '/_copy', + validate: { + body: schema.object({ + objects: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + }), + { maxSize: maxImportExportSize } + ) + ), + includeReferencesDeep: schema.boolean({ defaultValue: false }), + targetWorkspace: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const savedObjectsClient = context.core.savedObjects.client; + const { objects, includeReferencesDeep, targetWorkspace } = req.body; + + // need to access the registry for type validation, can't use the schema for this + const supportedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map((t) => t.name); + + if (objects) { + const validationError = validateObjects(objects, supportedTypes); + if (validationError) { + return res.badRequest({ + body: { + message: validationError, + }, + }); + } + } + + const objectsListStream = await exportSavedObjectsToStream({ + savedObjectsClient, + objects, + exportSizeLimit: maxImportExportSize, + includeReferencesDeep, + excludeExportDetails: true, + }); + + const result = await importSavedObjectsFromStream({ + savedObjectsClient: context.core.savedObjects.client, + typeRegistry: context.core.savedObjects.typeRegistry, + readStream: objectsListStream, + objectLimit: maxImportExportSize, + overwrite: true, + createNewCopies: true, + workspaces: [targetWorkspace], + }); + + return res.ok({ body: result }); + }) + ); +}; diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 7149474e446c..57dbe8f3ca7c 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -45,6 +45,7 @@ import { registerExportRoute } from './export'; import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; +import { registerCopyRoute } from './copy'; export function registerRoutes({ http, @@ -70,6 +71,7 @@ export function registerRoutes({ registerLogLegacyImportRoute(router, logger); registerExportRoute(router, config); registerImportRoute(router, config); + registerCopyRoute(router, config); registerResolveImportErrorsRoute(router, config); const internalRouter = http.createRouter('/internal/saved_objects/'); diff --git a/src/plugins/saved_objects_management/public/constants.ts b/src/plugins/saved_objects_management/public/constants.ts index dec0d4e7be68..e66d808dcf4c 100644 --- a/src/plugins/saved_objects_management/public/constants.ts +++ b/src/plugins/saved_objects_management/public/constants.ts @@ -29,3 +29,5 @@ export const SAVED_QUERIES_WORDINGS = i18n.translate( defaultMessage: 'Saved filters', } ); + +export const SAVED_OBJECT_TYPE_WORKSAPCE = 'workspace'; diff --git a/src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts new file mode 100644 index 000000000000..c28893589367 --- /dev/null +++ b/src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { HttpStart } from 'src/core/public'; + +export async function copySavedObjects( + http: HttpStart, + objects: any[], + includeReferencesDeep: boolean = true, + targetWorkspace: string +) { + return await http.post('/api/saved_objects/_copy', { + body: JSON.stringify({ + objects, + includeReferencesDeep, + targetWorkspace, + }), + }); +} diff --git a/src/plugins/saved_objects_management/public/lib/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts index fae58cad3eb2..7bb6f9168cbd 100644 --- a/src/plugins/saved_objects_management/public/lib/index.ts +++ b/src/plugins/saved_objects_management/public/lib/index.ts @@ -57,3 +57,4 @@ export { extractExportDetails, SavedObjectsExportResultDetails } from './extract export { createFieldList } from './create_field_list'; export { getAllowedTypes } from './get_allowed_types'; export { filterQuery } from './filter_query'; +export { copySavedObjects } from './copy_saved_objects'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/copy_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/copy_modal.tsx new file mode 100644 index 000000000000..80ee4d1c5894 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/copy_modal.tsx @@ -0,0 +1,320 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiComboBox, + EuiFormRow, + EuiSwitch, + EuiComboBoxOptionOption, + EuiInMemoryTable, + EuiToolTip, + EuiIcon, + EuiCallOut, +} from '@elastic/eui'; +import { WorkspaceAttribute, WorkspacesStart } from 'opensearch-dashboards/public'; +import { i18n } from '@osd/i18n'; +import { SavedObjectWithMetadata } from '../../../types'; +import { getSavedObjectLabel } from '../../../lib'; +import { SAVED_OBJECT_TYPE_WORKSAPCE } from '../../../constants'; + +type WorkspaceOption = EuiComboBoxOptionOption; + +interface Props { + workspacesStart: WorkspacesStart; + onCopy: (includeReferencesDeep: boolean, targetWorkspace: string) => Promise; + onClose: () => void; + seletedSavedObjects: SavedObjectWithMetadata[]; +} + +interface State { + ignoredSeletedObjects: SavedObjectWithMetadata[]; + allSeletedObjects: SavedObjectWithMetadata[]; + workspaceOptions: WorkspaceOption[]; + allWorkspaceOptions: WorkspaceOption[]; + targetWorkspaceOption: WorkspaceOption[]; + isLoading: boolean; + isIncludeReferencesDeepChecked: boolean; +} + +export class SavedObjectsCopyModal extends React.Component { + private isMounted = false; + + constructor(props: Props) { + super(props); + + this.state = { + ignoredSeletedObjects: [], + allSeletedObjects: this.props.seletedSavedObjects, + workspaceOptions: [], + allWorkspaceOptions: [], + targetWorkspaceOption: [], + isLoading: false, + isIncludeReferencesDeepChecked: true, + }; + } + + workspaceToOption = (workspace: WorkspaceAttribute): WorkspaceOption => { + return { label: workspace.name, key: workspace.id, value: workspace }; + }; + + async componentDidMount() { + const { workspacesStart } = this.props; + const workspaceList = await workspacesStart.client.workspaceList$; + const currentWorkspace = await workspacesStart.client.currentWorkspace$; + + if (!!currentWorkspace?.value?.name) { + const currentWorkspaceName = currentWorkspace.value.name; + const ignoredSeletedObjects = this.state.allSeletedObjects.filter( + (item) => + item.workspaces?.includes(currentWorkspaceName) || + item.type === SAVED_OBJECT_TYPE_WORKSAPCE + ); + const filteredWorkspaceOptions = workspaceList.value + .map(this.workspaceToOption) + .filter((item) => item.label !== currentWorkspaceName); + this.setState({ + workspaceOptions: filteredWorkspaceOptions, + allWorkspaceOptions: filteredWorkspaceOptions, + ignoredSeletedObjects, + }); + } else { + const allWorkspaceOptions = workspaceList.value.map(this.workspaceToOption); + this.setState({ + workspaceOptions: allWorkspaceOptions, + allWorkspaceOptions, + }); + } + + this.isMounted = true; + } + + componentWillUnmount() { + this.isMounted = false; + } + + copySavedObjects = async () => { + this.setState({ + isLoading: true, + }); + + const targetWorkspaceName = this.state.targetWorkspaceOption[0].label; + + await this.props.onCopy(this.state.isIncludeReferencesDeepChecked, targetWorkspaceName!); + + if (this.isMounted) { + this.setState({ + isLoading: false, + }); + } + }; + + onSearchWorkspaceChange = (searchValue: string) => { + this.setState({ + workspaceOptions: this.state.allWorkspaceOptions.filter((item) => + item.label.includes(searchValue) + ), + }); + }; + + onTargetWorkspaceChange = (targetWorkspaceOption: WorkspaceOption[]) => { + this.setState({ + targetWorkspaceOption, + }); + }; + + changeIncludeReferencesDeep = () => { + this.setState((state) => ({ + isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, + })); + }; + + render() { + const { + workspaceOptions, + targetWorkspaceOption, + isIncludeReferencesDeepChecked, + ignoredSeletedObjects, + allSeletedObjects, + } = this.state; + const includedSeletedObjects = allSeletedObjects.filter( + (item) => !ignoredSeletedObjects.some((ignoredItem) => item.id === ignoredItem.id) + ); + const ignoredSeletedObjectsLength = ignoredSeletedObjects.length; + + let confirmCopyButtonEnabled = false; + if ( + !!targetWorkspaceOption && + targetWorkspaceOption.length === 1 && + !!targetWorkspaceOption[0].label && + includedSeletedObjects.length > 0 + ) { + confirmCopyButtonEnabled = true; + } + + const warningMessageForOnlyOneSavedObject = ( +

    + 1 saved object will not be + copied, because it has already existed in the selected workspace or it is worksapce itself. +

    + ); + const warningMessageForMultipleSavedObjects = ( +

    + {ignoredSeletedObjectsLength} saved objects will{' '} + not be copied, because they have already existed in the + selected workspace or they are worksapces themselves. +

    + ); + + const ignoreSomeObjectsChildren: React.ReactChild = ( + <> + + {ignoredSeletedObjectsLength === 1 + ? warningMessageForOnlyOneSavedObject + : warningMessageForMultipleSavedObjects} + + + + ); + + return ( + + + + + + + + + + } + > + + + + + + } + checked={isIncludeReferencesDeepChecked} + onChange={this.changeIncludeReferencesDeep} + /> + + + {ignoredSeletedObjectsLength === 0 ? null : ignoreSomeObjectsChildren} +

    + +

    + + ( + + + + ), + }, + { + field: 'id', + name: i18n.translate('savedObjectsManagement.objectsTable.copyModal.idColumnName', { + defaultMessage: 'Id', + }), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.copyModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> +
    + + + + + + + + + + +
    + ); + } +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx index 9d46f1cca67c..176e605297f1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -43,15 +43,19 @@ import { FormattedMessage } from '@osd/i18n/react'; export const Header = ({ onExportAll, onImport, + onCopy, onRefresh, filteredCount, title, + selectedCount, }: { onExportAll: () => void; onImport: () => void; + onCopy: () => void; onRefresh: () => void; filteredCount: number; title: string; + selectedCount: number; }) => ( @@ -92,6 +96,20 @@ export const Header = ({ />
    + + + + + { let notifications: ReturnType; let savedObjects: ReturnType; let search: ReturnType['search']; + let workspacesStart: ReturnType; const shallowRender = (overrides: Partial = {}) => { return (shallowWithI18nProvider( @@ -121,6 +123,7 @@ describe('SavedObjectsTable', () => { notifications = notificationServiceMock.createStartContract(); savedObjects = savedObjectsServiceMock.createStartContract(); search = dataPluginMock.createStartContract().search; + workspacesStart = workspacesServiceMock.createStartContract(); const applications = applicationServiceMock.createStartContract(); applications.capabilities = { @@ -154,6 +157,7 @@ describe('SavedObjectsTable', () => { savedObjectsClient: savedObjects.client, indexPatterns: dataPluginMock.createStartContract().indexPatterns, http, + workspacesStart, overlays, notifications, applications, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 412047ba66f0..f39d760e87f8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -61,6 +61,7 @@ import { FormattedMessage } from '@osd/i18n/react'; import { SavedObjectsClientContract, SavedObjectsFindOptions, + WorkspacesStart, HttpStart, OverlayStart, NotificationsStart, @@ -81,6 +82,7 @@ import { findObject, extractExportDetails, SavedObjectsExportResultDetails, + copySavedObjects, } from '../../lib'; import { SavedObjectWithMetadata } from '../../types'; import { @@ -91,6 +93,7 @@ import { } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { SavedObjectsCopyModal } from './components/copy_modal'; interface ExportAllOption { id: string; @@ -106,6 +109,7 @@ export interface SavedObjectsTableProps { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; + workspacesStart: WorkspacesStart; search: DataPublicPluginStart['search']; overlays: OverlayStart; notifications: NotificationsStart; @@ -127,6 +131,7 @@ export interface SavedObjectsTableState { activeQuery: Query; selectedSavedObjects: SavedObjectWithMetadata[]; isShowingImportFlyout: boolean; + isShowingCopyModal: boolean; isSearching: boolean; filteredItemCount: number; isShowingRelationships: boolean; @@ -157,6 +162,7 @@ export class SavedObjectsTable extends Component { + const { selectedSavedObjects } = this.state; + const { notifications, http } = this.props; + const objectsToCopy = selectedSavedObjects.map((obj) => ({ id: obj.id, type: obj.type })); + + try { + await copySavedObjects(http, objectsToCopy, includeReferencesDeep, targetWorkspace); + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate('savedObjectsManagement.objectsTable.copy.dangerNotification', { + defaultMessage: 'Unable to copy saved objects', + }), + }); + throw e; + } + + this.hideCopyModal(); + this.refreshObjects(); + notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.copy.successNotification', { + defaultMessage: 'Copy saved objects successly', + }), + }); + }; + onExport = async (includeReferencesDeep: boolean) => { const { selectedSavedObjects } = this.state; const { notifications, http } = this.props; @@ -494,6 +525,14 @@ export class SavedObjectsTable extends Component { + this.setState({ isShowingCopyModal: true }); + }; + + hideCopyModal = () => { + this.setState({ isShowingCopyModal: false }); + }; + onDelete = () => { this.setState({ isShowingDeleteConfirmModal: true }); }; @@ -564,6 +603,23 @@ export class SavedObjectsTable extends Component + ); + } + renderRelationships() { if (!this.state.isShowingRelationships) { return null; @@ -857,12 +913,15 @@ export class SavedObjectsTable extends Component this.setState({ isShowingExportAllOptionsModal: true })} onImport={this.showImportFlyout} + onCopy={() => this.setState({ isShowingCopyModal: true })} onRefresh={this.refreshObjects} filteredCount={filteredItemCount} title={this.props.title} + selectedCount={selectedSavedObjects.length} /> diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index ec3837762317..2670e3cf0c91 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -89,6 +89,7 @@ const SavedObjectsTablePage = ({ indexPatterns={dataStart.indexPatterns} search={dataStart.search} http={coreStart.http} + workspacesStart={coreStart.workspaces} overlays={coreStart.overlays} notifications={coreStart.notifications} applications={coreStart.application}