diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 262b5025f4ff..3e9acfc9dd91 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, @@ -355,11 +361,4 @@ export { __osdBootstrap__ } from './osd_bootstrap'; export { WorkspacesStart, WorkspacesSetup, WorkspacesService } 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 d15ee2d538aa..9b58b7ef6d0d 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/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index 16916b9a41ad..cb7a62b3912a 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -1119,11 +1119,1481 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -2315,11 +3785,2151 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "dashboard", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -3511,11 +7121,274 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
+ > + + +
+ + + + } + body={ + +

+ +

+

+ + + , + } + } + /> +

+
+ } + iconType="dashboardApp" + title={ +

+ +

+ } + > +
+ + + + +
+ + +

+ + Create your first dashboard + +

+
+ + + +
+ + +
+

+ + You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. + +

+

+ + + , + } + } + > + New to OpenSearch Dashboards? + + + + to take a test drive. + +

+
+
+ + + +
+ + + + + + +
+ +
+ + +
@@ -4707,11 +8580,2111 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -5903,11 +11876,2231 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + > +
+
+ + + + Listing limit exceeded + + +
+ +
+ +
+

+ + + , + "entityNamePlural": "dashboards", + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalItems": 2, + } + } + > + You have 2 dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than 1. You can change this setting under + + + + Advanced Settings + + + + . + +

+
+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
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 1af8ac210696..40f72fac1c82 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/__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 9983d219de64..1108c447ff32 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 @@ -812,7 +812,6 @@ exports[`SavedObjectsTable should render normally 1`] = ` onExportAll={[Function]} onImport={[Function]} onRefresh={[Function]} - showDuplicateAll={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 dcdfc844f61b..92d7a4b49b6f 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 @@ -65,7 +65,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']; @@ -140,9 +140,7 @@ describe('SavedObjectsTable', () => { edit: false, delete: false, }, - workspaces: { - enabled: false, - }, + workspaces: {}, }; http.post.mockResolvedValue([]); @@ -392,7 +390,7 @@ describe('SavedObjectsTable', () => { allowedTypes, undefined, true, - {} + undefined ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ @@ -423,7 +421,7 @@ describe('SavedObjectsTable', () => { allowedTypes, 'test*', true, - {} + undefined ); expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ @@ -431,35 +429,11 @@ describe('SavedObjectsTable', () => { }); }); - it('should make modules call with workspace', async () => { - getSavedObjectCountsMock.mockClear(); - findObjectsMock.mockClear(); - // @ts-expect-error - defaultProps.applications.capabilities.workspaces.enabled = true; - const mockSelectedSavedObjects = [ - { id: '1', type: 'index-pattern' }, - { id: '3', type: 'dashboard' }, - ] as SavedObjectWithMetadata[]; - - const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ - _id: obj.id, - _type: obj.type, - _source: {}, - })); - - const mockSavedObjectsClient = { - ...defaultProps.savedObjectsClient, - bulkGet: jest.fn().mockImplementation(() => ({ - savedObjects: mockSavedObjects, - })), - }; - - const workspacesStart = workspacesServiceMock.createStartContract(); - workspacesStart.currentWorkspaceId$.next('foo'); + it('should export all, accounting for the current workspace criteria', async () => { + const component = shallowRender(); - const component = shallowRender({ - savedObjectsClient: mockSavedObjectsClient, - workspaces: workspacesStart, + component.instance().onQueryChange({ + query: Query.parse(`test workspaces:("${PUBLIC_WORKSPACE_NAME}")`), }); // Ensure all promises resolve @@ -467,32 +441,23 @@ describe('SavedObjectsTable', () => { // Ensure the state changes are reflected component.update(); - // Set some as selected - component.instance().onSelectionChanged(mockSelectedSavedObjects); + // Set up mocks + const blob = new Blob([JSON.stringify(allSavedObjects)], { type: 'application/ndjson' }); + fetchExportByTypeAndSearchMock.mockImplementation(() => blob); - await component.instance().onExport(true); await component.instance().onExportAll(); - expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true, { - workspaces: ['foo'], - }); expect(fetchExportByTypeAndSearchMock).toHaveBeenCalledWith( http, - ['index-pattern', 'visualization', 'dashboard', 'search'], - undefined, + allowedTypes, + 'test*', true, - { - workspaces: ['foo'], - } - ); - expect( - getSavedObjectCountsMock.mock.calls.every((item) => item[1].workspaces[0] === 'foo') - ).toEqual(true); - expect(findObjectsMock.mock.calls.every((item) => item[1].workspaces[0] === 'foo')).toEqual( - true + [PUBLIC_WORKSPACE_ID] ); - // @ts-expect-error - defaultProps.applications.capabilities.workspaces.enabled = false; + expect(saveAsMock).toHaveBeenCalledWith(blob, 'export.ndjson'); + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); }); }); @@ -668,7 +633,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: {}, @@ -713,10 +678,10 @@ 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('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: {}, @@ -761,7 +726,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 = { @@ -809,7 +774,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 = { @@ -849,8 +814,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/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 b88b4b84ee88..4b68ffb50569 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 @@ -96,6 +96,7 @@ import { } from '../../services'; import { Header, Table, Flyout, Relationships, SavedObjectsDuplicateModal } from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { formatWorkspaceIdParams } from '../../utils'; import { DuplicateMode } from '../types'; interface ExportAllOption { @@ -147,8 +148,8 @@ export interface SavedObjectsTableState { exportAllSelectedOptions: Record; isIncludeReferencesDeepChecked: boolean; currentWorkspaceId?: string; - availableWorkspaces?: WorkspaceAttribute[]; workspaceEnabled: boolean; + availableWorkspaces?: WorkspaceAttribute[]; } export class SavedObjectsTable extends Component { @@ -193,6 +194,44 @@ export class SavedObjectsTable extends Component ws.id).concat(PUBLIC_WORKSPACE_ID); + } else { + return [currentWorkspaceId]; + } + } + } + + private get workspaceNameIdLookup() { + const { availableWorkspaces } = this.state; + const workspaceNameIdMap = new Map(); + 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; + } + + /** + * convert workspace names to ids + * @param workspaceNames workspace name list + * @returns workspace id list + */ + private workspaceNamesToIds(workspaceNames?: string[]): string[] | undefined { + return workspaceNames + ?.map((wsName) => this.workspaceNameIdLookup.get(wsName) || '') + .filter((wsId) => !!wsId); + } + private get findOptions() { const { activeQuery: query, page, perPage } = this.state; const { allowedTypes, namespaceRegistry } = this.props; @@ -200,7 +239,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) { @@ -236,46 +265,10 @@ export class SavedObjectsTable extends Component ws.id).concat(PUBLIC_WORKSPACE_ID); - } else { - return [currentWorkspaceId]; - } - } - } - - private get workspaceNameIdLookup() { - const { availableWorkspaces } = this.state; - const workspaceNameIdMap = new Map(); - 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; - } - - private formatWorkspaceIdParams( - obj: T - ): T | Omit { - const { workspaces, ...others } = obj; - if (workspaces) { - return obj; - } - return others; - } - componentDidMount() { this._isMounted = true; - this.subscribleWorkspace(); + this.subscribeWorkspace(); this.fetchSavedObjects(); this.fetchCounts(); } @@ -298,7 +291,7 @@ export class SavedObjectsTable extends Component ns.id) || []; - const filteredCountOptions: SavedObjectCountOptions = this.formatWorkspaceIdParams({ + const filteredCountOptions: SavedObjectCountOptions = formatWorkspaceIdParams({ typesToInclude: filteredTypes, searchString: queryText, workspaces: this.workspaceIdQuery, @@ -309,9 +302,7 @@ export class SavedObjectsTable extends Component this.workspaceNameIdLookup?.get(wsName) || '') - .filter((wsId) => !!wsId); + filteredCountOptions.workspaces = this.workspaceNamesToIds(visibleWorkspaces); } // These are the saved objects visible in the table. @@ -336,7 +327,7 @@ export class SavedObjectsTable extends Component { + subscribeWorkspace = () => { const workspace = this.props.workspaces; this.currentWorkspaceIdSubscription = workspace.currentWorkspaceId$.subscribe((workspaceId) => this.setState({ @@ -517,7 +508,7 @@ export class SavedObjectsTable extends Component { if (selected) { accum.push(id); @@ -548,6 +539,9 @@ 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 000000000000..84727ab6a356 --- /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; +} diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts index 8d94c0d935c9..3f77457bc245 100644 --- a/src/plugins/saved_objects_management/server/routes/find.ts +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -29,7 +29,7 @@ */ import { schema } from '@osd/config-schema'; -import { IRouter } from 'src/core/server'; +import { IRouter, SavedObjectsFindOptions } from 'src/core/server'; import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources'; import { getIndexPatternTitle } from '../../../data/common/index_patterns/utils'; import { injectMetaAttributes } from '../lib'; @@ -67,7 +67,6 @@ export const registerFindRoute = ( workspaces: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) ), - workspacesSearchOperator: schema.maybe(schema.string()), }), }, }, @@ -94,12 +93,14 @@ export const registerFindRoute = ( return await client.get('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; + + 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 67a40df1501d..370c9c1a5d72 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/plugin.ts b/src/plugins/workspace/server/plugin.ts index 3b045c732e0f..7a971cc9f104 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -121,7 +121,7 @@ export class WorkspacePlugin implements Plugin { workspaces: ['foo'], }); }); + + it(`Should set workspacesSearchOperator to OR 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 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({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: workspaceEnabledMockRequest, + }); + await client.find({ + type: 'dashboard', + workspaces: ['bar', PUBLIC_WORKSPACE_ID], + }); + expect(mockedClient.find).toBeCalledWith({ + type: 'dashboard', + workspaces: ['bar'], + workspacesSearchOperator: 'OR', + }); + }); + + it(`Should not override workspacesSearchOperator when workspacesSearchOperator is specified`, 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 74e8e99af71e..b620b5556b77 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,8 @@ import { SavedObjectsCheckConflictsObject, OpenSearchDashboardsRequest, SavedObjectsFindOptions, + PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, } from '../../../../core/server'; type WorkspaceOptions = Pick | undefined; @@ -37,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, @@ -63,8 +74,31 @@ 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); + if (this.isWorkspaceType(findOptions.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 + ? findOptions.workspaces.indexOf(PUBLIC_WORKSPACE_ID) + : -1; + if (!findOptions.workspacesSearchOperator && findOptions.workspaces && index !== -1) { + findOptions.workspacesSearchOperator = 'OR'; + // remove this deletion logic when public workspace becomes to real + 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); + }, bulkGet: wrapperOptions.client.bulkGet, get: wrapperOptions.client.get, update: wrapperOptions.client.update, @@ -75,5 +109,5 @@ export class WorkspaceIdConsumerWrapper { }; }; - constructor() {} + constructor(private isPermissionControlEnabled?: boolean) {} }