From bafb4051b123d6569d75139628d4aa7e28a03d4f Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 11 Mar 2024 10:23:09 +0800 Subject: [PATCH 01/13] Add workspace filter into saved objects page (#211) * Add workspace column/filter into saved objects page Signed-off-by: Hailong Cui fix failed test case Signed-off-by: Hailong Cui move workspace column to its own folder Signed-off-by: Hailong Cui * default workspace Signed-off-by: Hailong Cui fix test case Signed-off-by: Hailong Cui add test case Signed-off-by: Hailong Cui remove hide import Signed-off-by: Hailong Cui * address review comments Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- src/core/public/index.ts | 2 +- src/core/server/index.ts | 2 +- src/core/utils/constants.ts | 6 + src/core/utils/index.ts | 7 +- .../public/lib/get_saved_object_counts.ts | 4 +- .../public/lib/parse_query.test.ts | 3 + .../public/lib/parse_query.ts | 7 + .../saved_objects_table.test.tsx | 194 ++++++++++++++++++ .../objects_table/saved_objects_table.tsx | 113 +++++++++- .../server/routes/find.ts | 1 + .../server/routes/scroll_count.ts | 28 ++- 11 files changed, 354 insertions(+), 13 deletions(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index cc51c721596..d475608ce08 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -359,4 +359,4 @@ export { __osdBootstrap__ } from './osd_bootstrap'; export { WorkspacesStart, WorkspacesSetup, WorkspacesService, WorkspaceObject } from './workspace'; -export { debounce } from './utils'; +export { debounce, DEFAULT_WORKSPACE_ID } from './utils'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 7cdc22c5d94..720d2aa0235 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -355,7 +355,7 @@ export { } from './metrics'; export { AppCategory, WorkspaceAttribute } from '../types'; -export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE } from '../utils'; +export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE, DEFAULT_WORKSPACE_ID } from '../utils'; export { SavedObject, diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index ecc1b7e863c..04566c20ba1 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -6,3 +6,9 @@ export const WORKSPACE_TYPE = 'workspace'; export const WORKSPACE_PATH_PREFIX = '/w'; + +/** + * deafult workspace is a virtual workspace, + * saved objects without any workspaces are consider belongs to default workspace + */ +export const DEFAULT_WORKSPACE_ID = 'default'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index a83f85a8fce..e2f5fd90460 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,5 +37,10 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; -export { WORKSPACE_PATH_PREFIX, WORKSPACE_TYPE } from './constants'; export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; +export { + WORKSPACE_PATH_PREFIX, + PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, + DEFAULT_WORKSPACE_ID, +} from './constants'; diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 6eaaac7d35f..9fb7b378cf1 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -39,8 +39,8 @@ export interface SavedObjectCountOptions { export async function getSavedObjectCounts( http: HttpStart, options: SavedObjectCountOptions -): Promise> { - return await http.post>( +): Promise>> { + return await http.post>>( `/api/opensearch-dashboards/management/saved_objects/scroll/counts`, { body: JSON.stringify(options) } ); diff --git a/src/plugins/saved_objects_management/public/lib/parse_query.test.ts b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts index a940cf3ebbc..731bb73a4d7 100644 --- a/src/plugins/saved_objects_management/public/lib/parse_query.test.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.test.ts @@ -39,6 +39,8 @@ describe('getQueryText', () => { return [{ value: 'lala' }, { value: 'lolo' }]; } else if (field === 'namespaces') { return [{ value: 'default' }]; + } else if (field === 'workspaces') { + return [{ value: 'workspaces' }]; } return []; }, @@ -47,6 +49,7 @@ describe('getQueryText', () => { queryText: 'foo bar', visibleTypes: 'lala', visibleNamespaces: 'default', + visibleWorkspaces: 'workspaces', }); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/parse_query.ts b/src/plugins/saved_objects_management/public/lib/parse_query.ts index 24c35d500aa..3db3f7fcee1 100644 --- a/src/plugins/saved_objects_management/public/lib/parse_query.ts +++ b/src/plugins/saved_objects_management/public/lib/parse_query.ts @@ -33,12 +33,15 @@ import { Query } from '@elastic/eui'; interface ParsedQuery { queryText?: string; visibleTypes?: string[]; + visibleNamespaces?: string[]; + visibleWorkspaces?: string[]; } export function parseQuery(query: Query): ParsedQuery { let queryText: string | undefined; let visibleTypes: string[] | undefined; let visibleNamespaces: string[] | undefined; + let visibleWorkspaces: string[] | undefined; if (query) { if (query.ast.getTermClauses().length) { @@ -53,11 +56,15 @@ export function parseQuery(query: Query): ParsedQuery { if (query.ast.getFieldClauses('namespaces')) { visibleNamespaces = query.ast.getFieldClauses('namespaces')[0].value as string[]; } + if (query.ast.getFieldClauses('workspaces')) { + visibleWorkspaces = query.ast.getFieldClauses('workspaces')[0].value as string[]; + } } return { queryText, visibleTypes, visibleNamespaces, + visibleWorkspaces, }; } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 74ae23c34dc..693c4bad9eb 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -40,6 +40,7 @@ import { import React from 'react'; import { Query } from '@elastic/eui'; +import { waitFor } from '@testing-library/dom'; import { ShallowWrapper } from 'enzyme'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; import { @@ -62,6 +63,9 @@ import { } from './saved_objects_table'; import { Flyout, Relationships } from './components'; import { SavedObjectWithMetadata } from '../../types'; +import { WorkspaceObject } from 'opensearch-dashboards/public'; +import { DEFAULT_WORKSPACE_ID } from '../../../../../core/public'; +import { TableProps } from './components/table'; const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; @@ -576,4 +580,194 @@ describe('SavedObjectsTable', () => { expect(component.state('selectedSavedObjects').length).toBe(0); }); }); + + describe('workspace filter', () => { + it('show workspace filter when workspace turn on and not in any workspace', async () => { + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + workspaces: { + enabled: true, + }, + }; + + const workspaceList: WorkspaceObject[] = [ + { + id: 'workspace1', + name: 'foo', + }, + { + id: 'workspace2', + name: 'bar', + }, + ]; + workspaces.workspaceList$.next(workspaceList); + workspaces.currentWorkspaceId$.next(''); + workspaces.currentWorkspace$.next(null); + + const component = shallowRender({ applications, workspaces }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + const props = component.find('Table').props() as TableProps; + const filters = props.filters; + expect(filters.length).toBe(2); + expect(filters[0].field).toBe('type'); + expect(filters[1].field).toBe('workspaces'); + expect(filters[1].options.length).toBe(3); + expect(filters[1].options[0].value).toBe('foo'); + expect(filters[1].options[1].value).toBe('bar'); + expect(filters[1].options[2].value).toBe(DEFAULT_WORKSPACE_ID); + }); + + it('show workspace filter when workspace turn on and enter a workspace', async () => { + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + workspaces: { + enabled: true, + }, + }; + + const workspaceList: WorkspaceObject[] = [ + { + id: 'workspace1', + name: 'foo', + }, + { + id: 'workspace2', + name: 'bar', + }, + ]; + workspaces.workspaceList$.next(workspaceList); + workspaces.currentWorkspaceId$.next('workspace1'); + workspaces.currentWorkspace$.next(workspaceList[0]); + + const component = shallowRender({ applications, workspaces }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + const props = component.find('Table').props() as TableProps; + const filters = props.filters; + const wsFilter = filters.filter((f) => f.field === 'workspaces'); + expect(wsFilter.length).toBe(1); + expect(wsFilter[0].options.length).toBe(1); + expect(wsFilter[0].options[0].value).toBe('foo'); + }); + + it('workspace exists in find options when workspace on', async () => { + findObjectsMock.mockClear(); + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + workspaces: { + enabled: true, + }, + }; + + const workspaceList: WorkspaceObject[] = [ + { + id: 'workspace1', + name: 'foo', + }, + { + id: 'workspace2', + name: 'bar', + }, + ]; + workspaces.workspaceList$.next(workspaceList); + workspaces.currentWorkspaceId$.next('workspace1'); + workspaces.currentWorkspace$.next(workspaceList[0]); + + const component = shallowRender({ applications, workspaces }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + await waitFor(() => { + expect(findObjectsMock).toBeCalledWith( + http, + expect.objectContaining({ + workspaces: expect.arrayContaining(['workspace1']), + }) + ); + }); + }); + + it('workspace exists in find options when workspace on and not in any workspace', async () => { + findObjectsMock.mockClear(); + const applications = applicationServiceMock.createStartContract(); + applications.capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + savedObjectsManagement: { + read: true, + edit: false, + delete: false, + }, + workspaces: { + enabled: true, + }, + }; + + const workspaceList: WorkspaceObject[] = [ + { + id: 'workspace1', + name: 'foo', + }, + { + id: 'workspace2', + name: 'bar', + }, + ]; + workspaces.workspaceList$.next(workspaceList); + + const component = shallowRender({ applications, workspaces }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + await waitFor(() => { + expect(findObjectsMock).toBeCalledWith( + http, + expect.objectContaining({ + workspaces: expect.arrayContaining(['workspace1', 'default']), + workspacesSearchOperator: expect.stringMatching('OR'), + }) + ); + }); + }); + }); }); 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 127ed08423e..c1d2a1a7ac4 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 @@ -69,6 +69,7 @@ import { WorkspaceAttribute, } from 'src/core/public'; import { Subscription } from 'rxjs'; +import { DEFAULT_WORKSPACE_ID } from '../../../../../core/public'; import { RedirectAppLinks } from '../../../../opensearch_dashboards_react/public'; import { IndexPatternsContract } from '../../../../data/public'; import { @@ -127,7 +128,7 @@ export interface SavedObjectsTableState { page: number; perPage: number; savedObjects: SavedObjectWithMetadata[]; - savedObjectCounts: Record; + savedObjectCounts: Record>; activeQuery: Query; selectedSavedObjects: SavedObjectWithMetadata[]; isShowingImportFlyout: boolean; @@ -142,6 +143,7 @@ export interface SavedObjectsTableState { exportAllSelectedOptions: Record; isIncludeReferencesDeepChecked: boolean; currentWorkspaceId?: string; + workspaceEnabled: boolean; availableWorkspaces?: WorkspaceAttribute[]; } export class SavedObjectsTable extends Component { @@ -152,15 +154,17 @@ export class SavedObjectsTable extends Component { + typeToCountMap[type] = 0; + return typeToCountMap; + }, {} as Record); + this.state = { totalCount: 0, page: 0, perPage: props.perPageConfig || 50, savedObjects: [], - savedObjectCounts: props.allowedTypes.reduce((typeToCountMap, type) => { - typeToCountMap[type] = 0; - return typeToCountMap; - }, {} as Record), + savedObjectCounts: { type: typeCounts } as Record>, activeQuery: Query.parse(''), selectedSavedObjects: [], isShowingImportFlyout: false, @@ -174,9 +178,48 @@ export class SavedObjectsTable extends Component ws.id).concat(DEFAULT_WORKSPACE_ID); + } else { + return [currentWorkspaceId]; + } + } + } + + private get wsNameIdLookup() { + const { availableWorkspaces } = this.state; + const workspaceNameIdMap = new Map(); + workspaceNameIdMap.set(DEFAULT_WORKSPACE_ID, DEFAULT_WORKSPACE_ID); + // Assumption: workspace name is unique across the system + availableWorkspaces?.reduce((map, ws) => { + return map.set(ws.name, ws.id); + }, workspaceNameIdMap); + return workspaceNameIdMap; + } + + private formatWorkspaceIdParams( + obj: T + ): T | Omit { + const { workspaces, ...others } = obj; + if (workspaces) { + return obj; + } + return others; + } + componentDidMount() { this._isMounted = true; this.subscribeWorkspace(); @@ -192,7 +235,9 @@ export class SavedObjectsTable extends Component { const { allowedTypes, namespaceRegistry } = this.props; - const { queryText, visibleTypes, visibleNamespaces } = parseQuery(this.state.activeQuery); + const { queryText, visibleTypes, visibleNamespaces, visibleWorkspaces } = parseQuery( + this.state.activeQuery + ); const filteredTypes = filterQuery(allowedTypes, visibleTypes); @@ -207,6 +252,11 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || '') + .filter((wsId) => !!wsId); + } // These are the saved objects visible in the table. const filteredSavedObjectCounts = await getSavedObjectCounts( @@ -280,7 +330,7 @@ export class SavedObjectsTable extends Component { const { activeQuery: query, page, perPage } = this.state; const { notifications, http, allowedTypes, namespaceRegistry } = this.props; - const { queryText, visibleTypes, visibleNamespaces } = parseQuery(query); + const { queryText, visibleTypes, visibleNamespaces, visibleWorkspaces } = parseQuery(query); const filteredTypes = filterQuery(allowedTypes, visibleTypes); // "searchFields" is missing from the "findOptions" but gets injected via the API. // The API extracts the fields from each uiExports.savedObjectsManagement "defaultSearchField" attribute @@ -298,6 +348,20 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || '' + ); + findOptions.workspaces = workspaceIds; + } + + if (findOptions.workspaces) { + if (findOptions.workspaces.indexOf(DEFAULT_WORKSPACE_ID) !== -1) { + // search both saved objects with workspace and without workspace + findOptions.workspacesSearchOperator = 'OR'; + } + } + if (findOptions.type.length > 1) { findOptions.sortField = 'type'; } @@ -830,6 +894,7 @@ export class SavedObjectsTable extends Component { + return this.workspaceIdQuery?.includes(ws.id); + }) + .map((ws) => { + return { + name: ws.name, + value: ws.name, + view: `${ws.name} (${wsCounts[ws.id] || 0})`, + }; + }); + + if (!currentWorkspaceId) { + wsFilterOptions.push({ + name: DEFAULT_WORKSPACE_ID, + value: DEFAULT_WORKSPACE_ID, + view: `Default (${wsCounts[DEFAULT_WORKSPACE_ID] || 0})`, + }); + } + + filters.push({ + type: 'field_value_selection', + field: 'workspaces', + name: i18n.translate('savedObjectsManagement.objectsTable.table.workspaceFilterName', { + defaultMessage: 'Workspaces', + }), + multiSelect: 'or', + options: wsFilterOptions, + }); + } + return ( {this.renderFlyout()} diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index dd49fc7575d..99cf50c9322 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -64,6 +64,7 @@ export const registerFindRoute = ( fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { defaultValue: [], }), + workspacesSearchOperator: schema.maybe(schema.string()), }), }, }, diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 63233748a89..1915b9be48e 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -30,6 +30,7 @@ import { schema } from '@osd/config-schema'; import { IRouter, SavedObjectsFindOptions } from 'src/core/server'; +import { DEFAULT_WORKSPACE_ID } from '../../../../core/server'; import { findAll } from '../lib'; export const registerScrollForCountRoute = (router: IRouter) => { @@ -46,7 +47,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { }, router.handleLegacyErrors(async (context, req, res) => { const { client } = context.core.savedObjects; - const counts = { + const counts: Record> = { type: {}, }; @@ -58,11 +59,22 @@ export const registerScrollForCountRoute = (router: IRouter) => { const requestHasNamespaces = Array.isArray(req.body.namespacesToInclude) && req.body.namespacesToInclude.length; + const requestHasWorkspaces = Array.isArray(req.body.workspaces) && req.body.workspaces.length; + if (requestHasNamespaces) { counts.namespaces = {}; findOptions.namespaces = req.body.namespacesToInclude; } + if (requestHasWorkspaces) { + counts.workspaces = {}; + findOptions.workspaces = req.body.workspaces; + if (findOptions.workspaces.indexOf(DEFAULT_WORKSPACE_ID) !== -1) { + // search both saved objects with workspace and without workspace + findOptions.workspacesSearchOperator = 'OR'; + } + } + if (req.body.searchString) { findOptions.search = `${req.body.searchString}*`; findOptions.searchFields = ['title']; @@ -82,6 +94,13 @@ export const registerScrollForCountRoute = (router: IRouter) => { counts.namespaces[ns]++; }); } + if (requestHasWorkspaces) { + const resultWorkspaces = result.workspaces || [DEFAULT_WORKSPACE_ID]; + resultWorkspaces.forEach((ws) => { + counts.workspaces[ws] = counts.workspaces[ws] || 0; + counts.workspaces[ws]++; + }); + } counts.type[type] = counts.type[type] || 0; counts.type[type]++; }); @@ -99,6 +118,13 @@ export const registerScrollForCountRoute = (router: IRouter) => { } } + const workspacesToInclude = req.body.workspaces || []; + for (const ws of workspacesToInclude) { + if (!counts.workspaces[ws]) { + counts.workspaces[ws] = 0; + } + } + return res.ok({ body: counts, }); From 081cc068e5bfb73640726718125c5113c321cde6 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 12 Apr 2024 11:28:38 +0800 Subject: [PATCH 02/13] replace default workspace with public workspace (#322) Signed-off-by: Hailong Cui --- src/core/public/index.ts | 9 ++++- src/core/server/index.ts | 7 +++- src/core/utils/constants.ts | 12 +++++-- src/core/utils/index.ts | 2 +- .../saved_objects_table.test.tsx | 6 ++-- .../objects_table/saved_objects_table.tsx | 33 ++++++++++--------- .../server/routes/scroll_count.ts | 6 ++-- 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index d475608ce08..31e3e33ff35 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -359,4 +359,11 @@ export { __osdBootstrap__ } from './osd_bootstrap'; export { WorkspacesStart, WorkspacesSetup, WorkspacesService, WorkspaceObject } from './workspace'; -export { debounce, DEFAULT_WORKSPACE_ID } from './utils'; +export { + WORKSPACE_TYPE, + cleanWorkspaceId, + PUBLIC_WORKSPACE_ID, + PUBLIC_WORKSPACE_NAME, +} from '../utils'; + +export { debounce } from './utils'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 720d2aa0235..8bd5deb3e6d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -355,7 +355,12 @@ export { } from './metrics'; export { AppCategory, WorkspaceAttribute } from '../types'; -export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE, DEFAULT_WORKSPACE_ID } from '../utils'; +export { + DEFAULT_APP_CATEGORIES, + PUBLIC_WORKSPACE_ID, + PUBLIC_WORKSPACE_NAME, + WORKSPACE_TYPE, +} from '../utils'; export { SavedObject, diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index 04566c20ba1..c05d2b06e04 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -3,12 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; + export const WORKSPACE_TYPE = 'workspace'; export const WORKSPACE_PATH_PREFIX = '/w'; /** - * deafult workspace is a virtual workspace, - * saved objects without any workspaces are consider belongs to default workspace + * public workspace has parity with global tenant, + * it includes saved objects with `public` as its workspace or without any workspce info */ -export const DEFAULT_WORKSPACE_ID = 'default'; +export const PUBLIC_WORKSPACE_ID = 'public'; + +export const PUBLIC_WORKSPACE_NAME = i18n.translate('workspaces.public.workspace.default.name', { + defaultMessage: 'Global workspace', +}); diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index e2f5fd90460..d15ee2d538a 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -41,6 +41,6 @@ export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } fro export { WORKSPACE_PATH_PREFIX, PUBLIC_WORKSPACE_ID, + PUBLIC_WORKSPACE_NAME, WORKSPACE_TYPE, - DEFAULT_WORKSPACE_ID, } from './constants'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 693c4bad9eb..b70314d7140 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -64,7 +64,7 @@ import { import { Flyout, Relationships } from './components'; import { SavedObjectWithMetadata } from '../../types'; import { WorkspaceObject } from 'opensearch-dashboards/public'; -import { DEFAULT_WORKSPACE_ID } from '../../../../../core/public'; +import { PUBLIC_WORKSPACE_ID } from '../../../../../core/public'; import { TableProps } from './components/table'; const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; @@ -627,7 +627,7 @@ describe('SavedObjectsTable', () => { expect(filters[1].options.length).toBe(3); expect(filters[1].options[0].value).toBe('foo'); expect(filters[1].options[1].value).toBe('bar'); - expect(filters[1].options[2].value).toBe(DEFAULT_WORKSPACE_ID); + expect(filters[1].options[2].value).toBe(PUBLIC_WORKSPACE_ID); }); it('show workspace filter when workspace turn on and enter a workspace', async () => { @@ -763,7 +763,7 @@ describe('SavedObjectsTable', () => { expect(findObjectsMock).toBeCalledWith( http, expect.objectContaining({ - workspaces: expect.arrayContaining(['workspace1', 'default']), + workspaces: expect.arrayContaining(['workspace1', PUBLIC_WORKSPACE_ID]), workspacesSearchOperator: expect.stringMatching('OR'), }) ); 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 c1d2a1a7ac4..f1ea4ab4ad1 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 @@ -69,7 +69,7 @@ import { WorkspaceAttribute, } from 'src/core/public'; import { Subscription } from 'rxjs'; -import { DEFAULT_WORKSPACE_ID } from '../../../../../core/public'; +import { PUBLIC_WORKSPACE_ID, PUBLIC_WORKSPACE_NAME } from '../../../../../core/public'; import { RedirectAppLinks } from '../../../../opensearch_dashboards_react/public'; import { IndexPatternsContract } from '../../../../data/public'; import { @@ -192,21 +192,21 @@ export class SavedObjectsTable extends Component ws.id).concat(DEFAULT_WORKSPACE_ID); + return availableWorkspaces?.map((ws) => ws.id).concat(PUBLIC_WORKSPACE_ID); } else { return [currentWorkspaceId]; } } } - private get wsNameIdLookup() { + private get workspaceNameIdLookup() { const { availableWorkspaces } = this.state; const workspaceNameIdMap = new Map(); - workspaceNameIdMap.set(DEFAULT_WORKSPACE_ID, DEFAULT_WORKSPACE_ID); - // Assumption: workspace name is unique across the system - availableWorkspaces?.reduce((map, ws) => { - return map.set(ws.name, ws.id); - }, workspaceNameIdMap); + workspaceNameIdMap.set(PUBLIC_WORKSPACE_NAME, PUBLIC_WORKSPACE_ID); + // workspace name is unique across the system + availableWorkspaces?.forEach((workspace) => { + workspaceNameIdMap.set(workspace.name, workspace.id); + }); return workspaceNameIdMap; } @@ -254,7 +254,7 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || '') + .map((wsName) => this.workspaceNameIdLookup?.get(wsName) || '') .filter((wsId) => !!wsId); } @@ -350,13 +350,13 @@ export class SavedObjectsTable extends Component this.wsNameIdLookup?.get(wsName) || '' + (wsName) => this.workspaceNameIdLookup?.get(wsName) || '' ); findOptions.workspaces = workspaceIds; } if (findOptions.workspaces) { - if (findOptions.workspaces.indexOf(DEFAULT_WORKSPACE_ID) !== -1) { + if (findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { // search both saved objects with workspace and without workspace findOptions.workspacesSearchOperator = 'OR'; } @@ -949,6 +949,8 @@ export class SavedObjectsTable extends Component workspace.id === PUBLIC_WORKSPACE_ID) > -1; const wsFilterOptions = availableWorkspaces .filter((ws) => { return this.workspaceIdQuery?.includes(ws.id); @@ -961,11 +963,12 @@ export class SavedObjectsTable extends Component { @@ -69,7 +69,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { if (requestHasWorkspaces) { counts.workspaces = {}; findOptions.workspaces = req.body.workspaces; - if (findOptions.workspaces.indexOf(DEFAULT_WORKSPACE_ID) !== -1) { + if (findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { // search both saved objects with workspace and without workspace findOptions.workspacesSearchOperator = 'OR'; } @@ -95,7 +95,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { }); } if (requestHasWorkspaces) { - const resultWorkspaces = result.workspaces || [DEFAULT_WORKSPACE_ID]; + const resultWorkspaces = result.workspaces || [PUBLIC_WORKSPACE_ID]; resultWorkspaces.forEach((ws) => { counts.workspaces[ws] = counts.workspaces[ws] || 0; counts.workspaces[ws]++; From 129198fa1d8d6e9c778892e06ab55a677506465c Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 15 Apr 2024 16:55:19 +0800 Subject: [PATCH 03/13] Add workspace filter Signed-off-by: Hailong Cui --- src/core/public/index.ts | 2 +- .../service/lib/search_dsl/query_params.ts | 27 +++++++--- .../lib/fetch_export_by_type_and_search.ts | 4 +- .../public/lib/get_saved_object_counts.ts | 1 + .../saved_objects_table.test.tsx.snap | 1 + .../saved_objects_table.test.tsx | 7 ++- .../objects_table/saved_objects_table.tsx | 54 +++++++++++-------- .../server/routes/find.ts | 19 +++++-- .../server/routes/scroll_count.ts | 1 + src/plugins/workspace/server/plugin.ts | 2 +- 10 files changed, 80 insertions(+), 38 deletions(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 31e3e33ff35..b7e022d616e 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -94,7 +94,7 @@ export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ export { CoreContext, CoreSystem } from './core_system'; -export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE, cleanWorkspaceId } from '../utils'; +export { DEFAULT_APP_CATEGORIES } from '../utils'; export { AppCategory, UiSettingsParams, 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 318768fd83c..d5a3a9576c1 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 @@ -151,6 +151,25 @@ function getClauseForWorkspace(workspace: string) { }; } +/** + * build DSL for workspace id terms query + * @param workspace workspace id list + * @returns workspace clause DSL + */ +function getClauseForWorkspaces(workspaces: string[]) { + if (workspaces.indexOf('*') !== -1) { + return { + match_all: {}, + }; + } + + return { + terms: { + workspaces: [...workspaces], + }, + }; +} + interface HasReferenceQueryParams { type: string; id: string; @@ -290,18 +309,14 @@ export function getQueryParams({ if (workspacesSearchOperator === 'OR') { ACLSearchParamsShouldClause.push({ bool: { - should: workspaces.map((workspace) => { - return getClauseForWorkspace(workspace); - }), + should: [getClauseForWorkspaces(workspaces)], minimum_should_match: 1, }, }); } else { bool.filter.push({ bool: { - should: workspaces.map((workspace) => { - return getClauseForWorkspace(workspace); - }), + should: [getClauseForWorkspaces(workspaces)], minimum_should_match: 1, }, }); diff --git a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts index e5f716347a7..1af8ac21069 100644 --- a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts @@ -34,10 +34,12 @@ export async function fetchExportByTypeAndSearch( http: HttpStart, types: string[], search: string | undefined, - includeReferencesDeep: boolean = false + includeReferencesDeep: boolean = false, + body?: Record ): Promise { return http.post('/api/saved_objects/_export', { body: JSON.stringify({ + ...body, type: types, search, includeReferencesDeep, diff --git a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts index 9fb7b378cf1..374f2720b53 100644 --- a/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts +++ b/src/plugins/saved_objects_management/public/lib/get_saved_object_counts.ts @@ -34,6 +34,7 @@ export interface SavedObjectCountOptions { typesToInclude: string[]; namespacesToInclude?: string[]; searchString?: string; + workspaces?: string[]; } export async function getSavedObjectCounts( 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 f074f9715c9..fdff7b9d913 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 @@ -231,6 +231,7 @@ exports[`SavedObjectsTable should render normally 1`] = ` "edit": false, "read": true, }, + "workspaces": Object {}, }, "currentAppId$": Observable { "_isScalar": false, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index b70314d7140..72ac725d3cd 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -139,6 +139,7 @@ describe('SavedObjectsTable', () => { edit: false, delete: false, }, + workspaces: {}, }; http.post.mockResolvedValue([]); @@ -371,7 +372,8 @@ describe('SavedObjectsTable', () => { http, allowedTypes, undefined, - true + true, + {} ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ @@ -401,7 +403,8 @@ describe('SavedObjectsTable', () => { http, allowedTypes, 'test*', - true + true, + {} ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ 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 f1ea4ab4ad1..9c86b66d4ca 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 @@ -192,6 +192,7 @@ export class SavedObjectsTable extends Component ws.id).concat(PUBLIC_WORKSPACE_ID); } else { return [currentWorkspaceId]; @@ -210,6 +211,17 @@ export class SavedObjectsTable extends Component this.workspaceNameIdLookup.get(wsName) || '') + .filter((wsId) => !!wsId); + } + private formatWorkspaceIdParams( obj: T ): T | Omit { @@ -243,19 +255,18 @@ export class SavedObjectsTable extends Component ns.id) || []; - const filteredCountOptions: SavedObjectCountOptions = { + const filteredCountOptions: SavedObjectCountOptions = this.formatWorkspaceIdParams({ typesToInclude: filteredTypes, searchString: queryText, - }; + workspaces: this.workspaceIdQuery, + }); if (availableNamespaces.length) { const filteredNamespaces = filterQuery(availableNamespaces, visibleNamespaces); filteredCountOptions.namespacesToInclude = filteredNamespaces; } if (visibleWorkspaces?.length) { - filteredCountOptions.workspaces = visibleWorkspaces - .map((wsName) => this.workspaceNameIdLookup?.get(wsName) || '') - .filter((wsId) => !!wsId); + filteredCountOptions.workspaces = this.workspaceNamesToIds(visibleWorkspaces); } // These are the saved objects visible in the table. @@ -280,10 +291,11 @@ export class SavedObjectsTable extends Component ns.id) || []; if (availableNamespaces.length) { @@ -349,17 +362,7 @@ export class SavedObjectsTable extends Component this.workspaceNameIdLookup?.get(wsName) || '' - ); - findOptions.workspaces = workspaceIds; - } - - if (findOptions.workspaces) { - if (findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { - // search both saved objects with workspace and without workspace - findOptions.workspacesSearchOperator = 'OR'; - } + findOptions.workspaces = this.workspaceNamesToIds(visibleWorkspaces); } if (findOptions.type.length > 1) { @@ -516,7 +519,7 @@ export class SavedObjectsTable extends Component { if (selected) { accum.push(id); @@ -524,13 +527,18 @@ export class SavedObjectsTable extends Component('data-source', id); }; - const findResponse = await client.find({ + const findOptions = { ...req.query, fields: undefined, searchFields: [...searchFields], - }); + workspaces: req.query.workspaces ? Array().concat(req.query.workspaces) : undefined, + } as SavedObjectsFindOptions; + + if (findOptions.workspaces && findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { + // search both saved objects with workspace and without workspace + findOptions.workspacesSearchOperator = 'OR'; + } + + const findResponse = await client.find(findOptions); const savedObjects = await Promise.all( findResponse.saved_objects.map(async (obj) => { diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index b24542ec893..67a40df1501 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -42,6 +42,7 @@ export const registerScrollForCountRoute = (router: IRouter) => { typesToInclude: schema.arrayOf(schema.string()), namespacesToInclude: schema.maybe(schema.arrayOf(schema.string())), searchString: schema.maybe(schema.string()), + workspaces: schema.maybe(schema.arrayOf(schema.string())), }), }, }, diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index db0921483eb..efc97c2bff1 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -68,7 +68,7 @@ export class WorkspacePlugin implements Plugin Date: Mon, 15 Apr 2024 18:24:56 +0800 Subject: [PATCH 04/13] revert query params change Signed-off-by: Hailong Cui --- .../service/lib/search_dsl/query_params.ts | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) 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 d5a3a9576c1..318768fd83c 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 @@ -151,25 +151,6 @@ function getClauseForWorkspace(workspace: string) { }; } -/** - * build DSL for workspace id terms query - * @param workspace workspace id list - * @returns workspace clause DSL - */ -function getClauseForWorkspaces(workspaces: string[]) { - if (workspaces.indexOf('*') !== -1) { - return { - match_all: {}, - }; - } - - return { - terms: { - workspaces: [...workspaces], - }, - }; -} - interface HasReferenceQueryParams { type: string; id: string; @@ -309,14 +290,18 @@ export function getQueryParams({ if (workspacesSearchOperator === 'OR') { ACLSearchParamsShouldClause.push({ bool: { - should: [getClauseForWorkspaces(workspaces)], + should: workspaces.map((workspace) => { + return getClauseForWorkspace(workspace); + }), minimum_should_match: 1, }, }); } else { bool.filter.push({ bool: { - should: [getClauseForWorkspaces(workspaces)], + should: workspaces.map((workspace) => { + return getClauseForWorkspace(workspace); + }), minimum_should_match: 1, }, }); From 1e0ab4be679b3ec9cf3c23a07944326b639b39f1 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 15 Apr 2024 18:46:56 +0800 Subject: [PATCH 05/13] udpate test case name to more clear Signed-off-by: Hailong Cui --- src/core/public/index.ts | 15 +++++++-------- src/core/utils/index.ts | 4 ++-- .../objects_table/saved_objects_table.test.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index b7e022d616e..849c954c126 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -94,7 +94,13 @@ export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ export { CoreContext, CoreSystem } from './core_system'; -export { DEFAULT_APP_CATEGORIES } from '../utils'; +export { + DEFAULT_APP_CATEGORIES, + WORKSPACE_TYPE, + cleanWorkspaceId, + PUBLIC_WORKSPACE_ID, + PUBLIC_WORKSPACE_NAME, +} from '../utils'; export { AppCategory, UiSettingsParams, @@ -359,11 +365,4 @@ export { __osdBootstrap__ } from './osd_bootstrap'; export { WorkspacesStart, WorkspacesSetup, WorkspacesService, WorkspaceObject } from './workspace'; -export { - WORKSPACE_TYPE, - cleanWorkspaceId, - PUBLIC_WORKSPACE_ID, - PUBLIC_WORKSPACE_NAME, -} from '../utils'; - export { debounce } from './utils'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index d15ee2d538a..9b58b7ef6d0 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,10 +37,10 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; -export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; export { WORKSPACE_PATH_PREFIX, + WORKSPACE_TYPE, PUBLIC_WORKSPACE_ID, PUBLIC_WORKSPACE_NAME, - WORKSPACE_TYPE, } from './constants'; +export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 72ac725d3cd..5f05d4b1051 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -585,7 +585,7 @@ describe('SavedObjectsTable', () => { }); describe('workspace filter', () => { - it('show workspace filter when workspace turn on and not in any workspace', async () => { + it('workspace filter include all visible workspaces when not in any workspace', async () => { const applications = applicationServiceMock.createStartContract(); applications.capabilities = { navLinks: {}, @@ -633,7 +633,7 @@ describe('SavedObjectsTable', () => { expect(filters[1].options[2].value).toBe(PUBLIC_WORKSPACE_ID); }); - it('show workspace filter when workspace turn on and enter a workspace', async () => { + it('workspace filter only include current workspaces when in a workspace', async () => { const applications = applicationServiceMock.createStartContract(); applications.capabilities = { navLinks: {}, @@ -678,7 +678,7 @@ describe('SavedObjectsTable', () => { expect(wsFilter[0].options[0].value).toBe('foo'); }); - it('workspace exists in find options when workspace on', async () => { + it('current workspace in find options when workspace on', async () => { findObjectsMock.mockClear(); const applications = applicationServiceMock.createStartContract(); applications.capabilities = { @@ -726,7 +726,7 @@ describe('SavedObjectsTable', () => { }); }); - it('workspace exists in find options when workspace on and not in any workspace', async () => { + it('all visible workspaces in find options when not in any workspace', async () => { findObjectsMock.mockClear(); const applications = applicationServiceMock.createStartContract(); applications.capabilities = { From cf2be4cb498c5ee065175bf54660f6ac799b2f07 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 15 Apr 2024 22:44:39 +0800 Subject: [PATCH 06/13] support public workspace Signed-off-by: Hailong Cui --- .../saved_objects_table.test.tsx | 7 +++-- .../server/routes/find.ts | 6 ----- .../server/routes/scroll_count.ts | 4 --- .../workspace_id_consumer_wrapper.test.ts | 26 +++++++++++++++++++ .../workspace_id_consumer_wrapper.ts | 16 ++++++++++-- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 5f05d4b1051..22d290f0411 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -64,7 +64,7 @@ import { import { Flyout, Relationships } from './components'; import { SavedObjectWithMetadata } from '../../types'; import { WorkspaceObject } from 'opensearch-dashboards/public'; -import { PUBLIC_WORKSPACE_ID } from '../../../../../core/public'; +import { PUBLIC_WORKSPACE_NAME, PUBLIC_WORKSPACE_ID } from '../../../../../core/public'; import { TableProps } from './components/table'; const allowedTypes = ['index-pattern', 'visualization', 'dashboard', 'search']; @@ -630,7 +630,7 @@ describe('SavedObjectsTable', () => { expect(filters[1].options.length).toBe(3); expect(filters[1].options[0].value).toBe('foo'); expect(filters[1].options[1].value).toBe('bar'); - expect(filters[1].options[2].value).toBe(PUBLIC_WORKSPACE_ID); + expect(filters[1].options[2].value).toBe(PUBLIC_WORKSPACE_NAME); }); it('workspace filter only include current workspaces when in a workspace', async () => { @@ -766,8 +766,7 @@ describe('SavedObjectsTable', () => { expect(findObjectsMock).toBeCalledWith( http, expect.objectContaining({ - workspaces: expect.arrayContaining(['workspace1', PUBLIC_WORKSPACE_ID]), - workspacesSearchOperator: expect.stringMatching('OR'), + workspaces: expect.arrayContaining(['workspace1', 'workspace2', PUBLIC_WORKSPACE_ID]), }) ); }); diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index 8e2b36a2244..3f77457bc24 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -34,7 +34,6 @@ import { DataSourceAttributes } from 'src/plugins/data_source/common/data_source import { getIndexPatternTitle } from '../../../data/common/index_patterns/utils'; import { injectMetaAttributes } from '../lib'; import { ISavedObjectsManagement } from '../services'; -import { PUBLIC_WORKSPACE_ID } from '../../../../core/server'; export const registerFindRoute = ( router: IRouter, @@ -101,11 +100,6 @@ export const registerFindRoute = ( workspaces: req.query.workspaces ? Array().concat(req.query.workspaces) : undefined, } as SavedObjectsFindOptions; - if (findOptions.workspaces && findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { - // search both saved objects with workspace and without workspace - findOptions.workspacesSearchOperator = 'OR'; - } - const findResponse = await client.find(findOptions); const savedObjects = await Promise.all( diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts index 67a40df1501..370c9c1a5d7 100644 --- a/src/plugins/saved_objects_management/server/routes/scroll_count.ts +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -70,10 +70,6 @@ export const registerScrollForCountRoute = (router: IRouter) => { if (requestHasWorkspaces) { counts.workspaces = {}; findOptions.workspaces = req.body.workspaces; - if (findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1) { - // search both saved objects with workspace and without workspace - findOptions.workspacesSearchOperator = 'OR'; - } } if (req.body.searchString) { diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index 112f31baf56..7e919950dae 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -5,6 +5,7 @@ import { updateWorkspaceState } from '../../../../core/server/utils'; import { SavedObject } from '../../../../core/public'; +import { PUBLIC_WORKSPACE_ID } from '../../../../core/server'; import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks'; import { WorkspaceIdConsumerWrapper } from './workspace_id_consumer_wrapper'; @@ -114,5 +115,30 @@ describe('WorkspaceIdConsumerWrapper', () => { workspaces: ['foo'], }); }); + + it(`Should set workspacesSearchOperator when search with public workspace`, async () => { + await wrapperClient.find({ + type: 'dashboard', + workspaces: ['bar', PUBLIC_WORKSPACE_ID], + }); + expect(mockedClient.find).toBeCalledWith({ + type: 'dashboard', + workspaces: ['bar', PUBLIC_WORKSPACE_ID], + workspacesSearchOperator: 'OR', + }); + }); + + it(`Should not override workspacesSearchOperator when search with public workspace`, async () => { + await wrapperClient.find({ + type: 'dashboard', + workspaces: [PUBLIC_WORKSPACE_ID], + workspacesSearchOperator: 'AND', + }); + expect(mockedClient.find).toBeCalledWith({ + type: 'dashboard', + workspaces: [PUBLIC_WORKSPACE_ID], + workspacesSearchOperator: 'AND', + }); + }); }); }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 74e8e99af71..586a77e1c88 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -12,6 +12,7 @@ import { SavedObjectsCheckConflictsObject, OpenSearchDashboardsRequest, SavedObjectsFindOptions, + PUBLIC_WORKSPACE_ID, } from '../../../../core/server'; type WorkspaceOptions = Pick | undefined; @@ -63,8 +64,19 @@ export class WorkspaceIdConsumerWrapper { this.formatWorkspaceIdParams(wrapperOptions.request, options) ), delete: wrapperOptions.client.delete, - find: (options: SavedObjectsFindOptions) => - wrapperOptions.client.find(this.formatWorkspaceIdParams(wrapperOptions.request, options)), + find: (options: SavedObjectsFindOptions) => { + const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); + // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `public` workspace + // remove the condition until when public workspace is real, and we have all data migrated to global workspace + if ( + !findOptions.workspacesSearchOperator && + findOptions.workspaces && + findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1 + ) { + findOptions.workspacesSearchOperator = 'OR'; + } + return wrapperOptions.client.find(findOptions); + }, bulkGet: wrapperOptions.client.bulkGet, get: wrapperOptions.client.get, update: wrapperOptions.client.update, From a67f31ecc98405c6415ae5c9d9cbd21800a88e39 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 15 Apr 2024 22:58:26 +0800 Subject: [PATCH 07/13] Add changelog Signed-off-by: Hailong Cui --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d4ec919e3..6852cfd1daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Dynamic Configurations] Improve dynamic configurations by adding cache and simplifying client fetch ([#6364](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6364)) - [MD] Add OpenSearch cluster group label to top of single selectable dropdown ([#6400](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6400)) - [Workspace] Support workspace in saved objects client in server side. ([#6365](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6365)) +- [Workspace] Add workspaces filter to saved objects page. ([#6458](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6458)) ### 🐛 Bug Fixes From 48bdfb343599a65af76d9050333c658aa2a91409 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 15 Apr 2024 23:59:08 +0800 Subject: [PATCH 08/13] update fetchExportByTypeAndSearch parameter Co-authored-by: SuZhou-Joe Signed-off-by: Hailong Cui --- .../lib/fetch_export_by_type_and_search.ts | 19 +++++----- .../saved_objects_table.test.tsx | 35 +++++++++++++++++-- .../objects_table/saved_objects_table.tsx | 22 ++++-------- .../public/utils.test.ts | 20 +++++++++++ .../saved_objects_management/public/utils.ts | 14 ++++++++ 5 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 src/plugins/saved_objects_management/public/utils.test.ts create mode 100644 src/plugins/saved_objects_management/public/utils.ts diff --git a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts index 1af8ac21069..40f72fac1c8 100644 --- a/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts +++ b/src/plugins/saved_objects_management/public/lib/fetch_export_by_type_and_search.ts @@ -28,21 +28,24 @@ * under the License. */ -import { HttpStart } from 'src/core/public'; +import { HttpStart, SavedObjectsBaseOptions } from 'src/core/public'; +import { formatWorkspaceIdParams } from '../utils'; export async function fetchExportByTypeAndSearch( http: HttpStart, types: string[], search: string | undefined, includeReferencesDeep: boolean = false, - body?: Record + workspaces: SavedObjectsBaseOptions['workspaces'] ): Promise { return http.post('/api/saved_objects/_export', { - body: JSON.stringify({ - ...body, - type: types, - search, - includeReferencesDeep, - }), + body: JSON.stringify( + formatWorkspaceIdParams({ + workspaces, + type: types, + search, + includeReferencesDeep, + }) + ), }); } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 22d290f0411..7c6f67e741c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -373,7 +373,7 @@ describe('SavedObjectsTable', () => { allowedTypes, undefined, true, - {} + undefined ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ @@ -404,7 +404,38 @@ describe('SavedObjectsTable', () => { allowedTypes, 'test*', true, - {} + undefined + ); + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); + }); + + it('should export all, accounting for the current workspace criteria', async () => { + const component = shallowRender(); + + component.instance().onQueryChange({ + query: Query.parse(`test workspaces:("${PUBLIC_WORKSPACE_NAME}")`), + }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set up mocks + const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); + + await component.instance().onExportAll(); + + expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( + http, + allowedTypes, + 'test*', + true, + [PUBLIC_WORKSPACE_ID] ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ 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 9c86b66d4ca..5533359e345 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 @@ -95,6 +95,7 @@ import { } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { formatWorkspaceIdParams } from '../../utils'; interface ExportAllOption { id: string; @@ -222,16 +223,6 @@ export class SavedObjectsTable extends Component !!wsId); } - private formatWorkspaceIdParams( - obj: T - ): T | Omit { - const { workspaces, ...others } = obj; - if (workspaces) { - return obj; - } - return others; - } - componentDidMount() { this._isMounted = true; this.subscribeWorkspace(); @@ -255,7 +246,7 @@ export class SavedObjectsTable extends Component ns.id) || []; - const filteredCountOptions: SavedObjectCountOptions = this.formatWorkspaceIdParams({ + const filteredCountOptions: SavedObjectCountOptions = formatWorkspaceIdParams({ typesToInclude: filteredTypes, searchString: queryText, workspaces: this.workspaceIdQuery, @@ -291,7 +282,7 @@ export class SavedObjectsTable extends Component { + it('formatWorkspaceIdParams with workspace null/undefined', async () => { + let obj = formatWorkspaceIdParams({ foo: 'bar', workspaces: null }); + expect(obj).not.toHaveProperty('workspaces'); + obj = formatWorkspaceIdParams({ foo: 'bar', workspaces: undefined }); + expect(obj).not.toHaveProperty('workspaces'); + }); + + it('formatWorkspaceIdParams with workspace exists', async () => { + const obj = formatWorkspaceIdParams({ foo: 'bar', workspaces: ['foo'] }); + expect(obj).toEqual({ foo: 'bar', workspaces: ['foo'] }); + }); +}); diff --git a/src/plugins/saved_objects_management/public/utils.ts b/src/plugins/saved_objects_management/public/utils.ts new file mode 100644 index 00000000000..84727ab6a35 --- /dev/null +++ b/src/plugins/saved_objects_management/public/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function formatWorkspaceIdParams( + obj: T +): T | Omit { + const { workspaces, ...others } = obj; + if (workspaces) { + return obj; + } + return others; +} From 5445bd0dc3bfca5080d6aac5f5197a76bd6de89e Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 16 Apr 2024 16:29:37 +0800 Subject: [PATCH 09/13] remove virtrual public workspace Signed-off-by: Hailong Cui --- .../workspace_id_consumer_wrapper.test.ts | 2 +- .../saved_objects/workspace_id_consumer_wrapper.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index 7e919950dae..42db1cd66cd 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -123,7 +123,7 @@ describe('WorkspaceIdConsumerWrapper', () => { }); expect(mockedClient.find).toBeCalledWith({ type: 'dashboard', - workspaces: ['bar', PUBLIC_WORKSPACE_ID], + workspaces: ['bar'], workspacesSearchOperator: 'OR', }); }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 586a77e1c88..43f2a9bf80d 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -68,11 +68,11 @@ export class WorkspaceIdConsumerWrapper { const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `public` workspace // remove the condition until when public workspace is real, and we have all data migrated to global workspace - if ( - !findOptions.workspacesSearchOperator && - findOptions.workspaces && - findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) !== -1 - ) { + const index = findOptions.workspaces?.indexOf(PUBLIC_WORKSPACE_ID) || -1; + if (!findOptions.workspacesSearchOperator && findOptions.workspaces && index !== -1) { + // remove public workspace to make sure we can pass permission control + // remove this logic when public workspace becomes to real + findOptions.workspaces.splice(index, 1); findOptions.workspacesSearchOperator = 'OR'; } return wrapperOptions.client.find(findOptions); From 380de157e92a3f4f2feb941b71270cbbc7ae6d80 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 16 Apr 2024 16:50:50 +0800 Subject: [PATCH 10/13] update comments Signed-off-by: Hailong Cui --- .../saved_objects/workspace_id_consumer_wrapper.test.ts | 2 +- .../saved_objects/workspace_id_consumer_wrapper.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index 42db1cd66cd..caec663a3f5 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -128,7 +128,7 @@ describe('WorkspaceIdConsumerWrapper', () => { }); }); - it(`Should not override workspacesSearchOperator when search with public workspace`, async () => { + it(`Should not override workspacesSearchOperator when workspacesSearchOperator is specified`, async () => { await wrapperClient.find({ type: 'dashboard', workspaces: [PUBLIC_WORKSPACE_ID], diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 43f2a9bf80d..b6fe022744e 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -66,14 +66,13 @@ export class WorkspaceIdConsumerWrapper { delete: wrapperOptions.client.delete, find: (options: SavedObjectsFindOptions) => { const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); - // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `public` workspace - // remove the condition until when public workspace is real, and we have all data migrated to global workspace + // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `PUBLIC_WORKSPACE_ID` workspace const index = findOptions.workspaces?.indexOf(PUBLIC_WORKSPACE_ID) || -1; if (!findOptions.workspacesSearchOperator && findOptions.workspaces && index !== -1) { - // remove public workspace to make sure we can pass permission control - // remove this logic when public workspace becomes to real - findOptions.workspaces.splice(index, 1); findOptions.workspacesSearchOperator = 'OR'; + // remove public workspace to make sure we can pass permission control validation, more details in `WorkspaceSavedObjectsClientWrapper` + // remove this deletion logic when public workspace becomes to real + findOptions.workspaces.splice(index, 1); } return wrapperOptions.client.find(findOptions); }, From 49b1617d26d147000a3f16f423d3da6c073694bb Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 16 Apr 2024 18:45:43 +0800 Subject: [PATCH 11/13] fix public workspace query when permission is not enabled Signed-off-by: Hailong Cui --- src/plugins/workspace/server/plugin.ts | 2 +- .../workspace_id_consumer_wrapper.test.ts | 18 ++++++++++++++++++ .../workspace_id_consumer_wrapper.ts | 13 +++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index efc97c2bff1..0056e9ac784 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -93,7 +93,7 @@ export class WorkspacePlugin implements Plugin { it(`Should set workspacesSearchOperator when search with public workspace`, async () => { await wrapperClient.find({ + type: 'dashboard', + workspaces: [PUBLIC_WORKSPACE_ID], + }); + expect(mockedClient.find).toBeCalledWith({ + type: 'dashboard', + workspaces: [PUBLIC_WORKSPACE_ID], + workspacesSearchOperator: 'OR', + }); + }); + + it(`Should remove public workspace when permission control is enabled`, async () => { + const consumer = new WorkspaceIdConsumerWrapper(true); + const client = consumer.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: workspaceEnabledMockRequest, + }); + await client.find({ type: 'dashboard', workspaces: ['bar', PUBLIC_WORKSPACE_ID], }); diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index b6fe022744e..1df746f3f22 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -66,13 +66,18 @@ export class WorkspaceIdConsumerWrapper { delete: wrapperOptions.client.delete, find: (options: SavedObjectsFindOptions) => { const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); + // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `PUBLIC_WORKSPACE_ID` workspace - const index = findOptions.workspaces?.indexOf(PUBLIC_WORKSPACE_ID) || -1; + const index = findOptions.workspaces + ? findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) + : -1; if (!findOptions.workspacesSearchOperator && findOptions.workspaces && index !== -1) { findOptions.workspacesSearchOperator = 'OR'; - // remove public workspace to make sure we can pass permission control validation, more details in `WorkspaceSavedObjectsClientWrapper` // remove this deletion logic when public workspace becomes to real - findOptions.workspaces.splice(index, 1); + if (this.isPermissionControlEnabled) { + // remove public workspace to make sure we can pass permission control validation, more details in `WorkspaceSavedObjectsClientWrapper` + findOptions.workspaces.splice(index, 1); + } } return wrapperOptions.client.find(findOptions); }, @@ -86,5 +91,5 @@ export class WorkspaceIdConsumerWrapper { }; }; - constructor() {} + constructor(private isPermissionControlEnabled?: boolean) {} } From 18edfb8d7b680e0195bae3ec380d5935663743fd Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 16 Apr 2024 23:26:30 +0800 Subject: [PATCH 12/13] home dashboards/listing page only show public workspace data Signed-off-by: Hailong Cui --- .../saved_objects/workspace_id_consumer_wrapper.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index 1df746f3f22..d6b0908f78e 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -13,6 +13,7 @@ import { OpenSearchDashboardsRequest, SavedObjectsFindOptions, PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, } from '../../../../core/server'; type WorkspaceOptions = Pick | undefined; @@ -66,6 +67,14 @@ export class WorkspaceIdConsumerWrapper { delete: wrapperOptions.client.delete, find: (options: SavedObjectsFindOptions) => { const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); + if (findOptions.type && findOptions.type === WORKSPACE_TYPE) { + return wrapperOptions.client.find(findOptions); + } + + // if workspace is enabled, we always find by workspace + if (!findOptions.workspaces || findOptions.workspaces.length === 0) { + findOptions.workspaces = [PUBLIC_WORKSPACE_ID]; + } // `PUBLIC_WORKSPACE_ID` includes both saved objects without any workspace and with `PUBLIC_WORKSPACE_ID` workspace const index = findOptions.workspaces From 35200143074fe9671d608a0d7edd5ab6d70cda09 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 17 Apr 2024 11:30:16 +0800 Subject: [PATCH 13/13] Add more test case Signed-off-by: Hailong Cui --- .../workspace_id_consumer_wrapper.test.ts | 20 ++++++++++++++++++- .../workspace_id_consumer_wrapper.ts | 11 +++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts index a9b83fb8a49..85fb1c4dfd9 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.test.ts @@ -116,7 +116,7 @@ describe('WorkspaceIdConsumerWrapper', () => { }); }); - it(`Should set workspacesSearchOperator when search with public workspace`, async () => { + it(`Should set workspacesSearchOperator to OR when search with public workspace`, async () => { await wrapperClient.find({ type: 'dashboard', workspaces: [PUBLIC_WORKSPACE_ID], @@ -128,6 +128,24 @@ describe('WorkspaceIdConsumerWrapper', () => { }); }); + it(`Should set workspace as pubic when workspace is not specified`, async () => { + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); + updateWorkspaceState(mockRequest, {}); + const mockedWrapperClient = wrapperInstance.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: mockRequest, + }); + await mockedWrapperClient.find({ + type: ['dashboard', 'visualization'], + }); + expect(mockedClient.find).toBeCalledWith({ + type: ['dashboard', 'visualization'], + workspaces: [PUBLIC_WORKSPACE_ID], + workspacesSearchOperator: 'OR', + }); + }); + it(`Should remove public workspace when permission control is enabled`, async () => { const consumer = new WorkspaceIdConsumerWrapper(true); const client = consumer.wrapperFactory({ diff --git a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts index d6b0908f78e..b620b5556b7 100644 --- a/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts +++ b/src/plugins/workspace/server/saved_objects/workspace_id_consumer_wrapper.ts @@ -39,6 +39,15 @@ export class WorkspaceIdConsumerWrapper { ...(finalWorkspaces.length ? { workspaces: finalWorkspaces } : {}), }; } + + private isWorkspaceType(type: SavedObjectsFindOptions['type']): boolean { + if (Array.isArray(type)) { + return type.every((item) => item === WORKSPACE_TYPE); + } + + return type === WORKSPACE_TYPE; + } + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { return { ...wrapperOptions.client, @@ -67,7 +76,7 @@ export class WorkspaceIdConsumerWrapper { delete: wrapperOptions.client.delete, find: (options: SavedObjectsFindOptions) => { const findOptions = this.formatWorkspaceIdParams(wrapperOptions.request, options); - if (findOptions.type && findOptions.type === WORKSPACE_TYPE) { + if (this.isWorkspaceType(findOptions.type)) { return wrapperOptions.client.find(findOptions); }