From fbecea85a9b9bc626d7ff7b0dc04442328cca483 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 13 Oct 2023 11:07:57 +0800 Subject: [PATCH] [Workspace][Feature] Import sample data to workspace (#210) * feat: import sample data saved objects to workspace Signed-off-by: Lin Wang * refactor: simplify sample data saved object id prefix logic (#1) * refactor: simplify sample data saved object id prefix logic Signed-off-by: Yulong Ruan * fix: align the prefix order of sample data install and uninstall rename appendPrefix to addPrefix Signed-off-by: Yulong Ruan --------- Signed-off-by: Yulong Ruan * refactor: assigned copied saved objects to new variables Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang Signed-off-by: Yulong Ruan Co-authored-by: Yulong Ruan --- .../components/sample_data_set_cards.js | 19 ++- .../opensearch_dashboards_services.ts | 2 + .../public/application/sample_data_client.js | 23 +++- src/plugins/home/public/plugin.ts | 1 + .../sample_data/data_sets/ecommerce/index.ts | 8 +- .../sample_data/data_sets/flights/index.ts | 8 +- .../sample_data/data_sets/logs/index.ts | 8 +- .../services/sample_data/data_sets/util.ts | 124 +++++++++++------- .../lib/sample_dataset_registry_types.ts | 6 +- .../sample_data/routes/install.test.ts | 63 +++++++++ .../services/sample_data/routes/install.ts | 22 +++- .../services/sample_data/routes/list.test.ts | 105 +++++++++++++++ .../services/sample_data/routes/list.ts | 8 +- .../sample_data/routes/uninstall.test.ts | 31 +++++ .../services/sample_data/routes/uninstall.ts | 16 ++- 15 files changed, 356 insertions(+), 88 deletions(-) diff --git a/src/plugins/home/public/application/components/sample_data_set_cards.js b/src/plugins/home/public/application/components/sample_data_set_cards.js index 5d1e2b86f149..a98e281083b7 100644 --- a/src/plugins/home/public/application/components/sample_data_set_cards.js +++ b/src/plugins/home/public/application/components/sample_data_set_cards.js @@ -81,7 +81,10 @@ export class SampleDataSetCards extends React.Component { loadSampleDataSets = async (dataSourceId) => { let sampleDataSets; try { - sampleDataSets = await listSampleDataSets(dataSourceId); + sampleDataSets = await listSampleDataSets( + dataSourceId, + getServices().workspaces.currentWorkspaceId$.getValue() + ); } catch (fetchError) { this.toastNotifications.addDanger({ title: i18n.translate('home.sampleDataSet.unableToLoadListErrorMessage', { @@ -114,7 +117,12 @@ export class SampleDataSetCards extends React.Component { })); try { - await installSampleDataSet(id, targetSampleDataSet.defaultIndex, dataSourceId); + await installSampleDataSet( + id, + targetSampleDataSet.defaultIndex, + dataSourceId, + getServices().workspaces.currentWorkspaceId$.getValue() + ); } catch (fetchError) { if (this._isMounted) { this.setState((prevState) => ({ @@ -162,7 +170,12 @@ export class SampleDataSetCards extends React.Component { })); try { - await uninstallSampleDataSet(id, targetSampleDataSet.defaultIndex, dataSourceId); + await uninstallSampleDataSet( + id, + targetSampleDataSet.defaultIndex, + dataSourceId, + getServices().workspaces.currentWorkspaceId$.getValue() + ); } catch (fetchError) { if (this._isMounted) { this.setState((prevState) => ({ diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index 60f9e70621ff..ac918c005db2 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -37,6 +37,7 @@ import { SavedObjectsClientContract, IUiSettingsClient, ApplicationStart, + WorkspaceStart, } from 'opensearch-dashboards/public'; import { UiStatsMetricType } from '@osd/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; @@ -73,6 +74,7 @@ export interface HomeOpenSearchDashboardsServices { getBranding: () => HomePluginBranding; }; dataSource?: DataSourcePluginStart; + workspaces: WorkspaceStart; } let services: HomeOpenSearchDashboardsServices | null = null; diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js index 045736c428f6..7334c14a7033 100644 --- a/src/plugins/home/public/application/sample_data_client.js +++ b/src/plugins/home/public/application/sample_data_client.js @@ -36,13 +36,13 @@ function clearIndexPatternsCache() { getServices().indexPatternService.clearCache(); } -export async function listSampleDataSets(dataSourceId) { - const query = buildQuery(dataSourceId); +export async function listSampleDataSets(dataSourceId, workspaceId) { + const query = buildQuery(dataSourceId, workspaceId); return await getServices().http.get(sampleDataUrl, { query }); } -export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { - const query = buildQuery(dataSourceId); +export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId, workspaceId) { + const query = buildQuery(dataSourceId, workspaceId); await getServices().http.post(`${sampleDataUrl}/${id}`, { query }); if (getServices().uiSettings.isDefault('defaultIndex')) { @@ -52,8 +52,13 @@ export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourc clearIndexPatternsCache(); } -export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { - const query = buildQuery(dataSourceId); +export async function uninstallSampleDataSet( + id, + sampleDataDefaultIndex, + dataSourceId, + workspaceId +) { + const query = buildQuery(dataSourceId, workspaceId); await getServices().http.delete(`${sampleDataUrl}/${id}`, { query }); const uiSettings = getServices().uiSettings; @@ -68,12 +73,16 @@ export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSou clearIndexPatternsCache(); } -function buildQuery(dataSourceId) { +function buildQuery(dataSourceId, workspaceId) { const query = {}; if (dataSourceId) { query.data_source_id = dataSourceId; } + if (workspaceId) { + query.workspace_id = workspaceId; + } + return query; } diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 1538156a801e..bf815a30c74d 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -122,6 +122,7 @@ export class HomePublicPlugin featureCatalogue: this.featuresCatalogueRegistry, injectedMetadata: coreStart.injectedMetadata, dataSource, + workspaces: coreStart.workspaces, }); coreStart.chrome.docTitle.change( i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts index 75e9ea50ff87..1a4ebd2a5e72 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/index.ts @@ -33,7 +33,7 @@ import { i18n } from '@osd/i18n'; import { getSavedObjects } from './saved_objects'; import { fieldMappings } from './field_mappings'; import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types'; -import { getSavedObjectsWithDataSource, appendDataSourceId } from '../util'; +import { addPrefixTo } from '../util'; const ecommerceName = i18n.translate('home.sampleData.ecommerceSpecTitle', { defaultMessage: 'Sample eCommerce orders', @@ -55,13 +55,11 @@ export const ecommerceSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), + getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), savedObjects: getSavedObjects(), - getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => - getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), dataIndices: [ { id: 'ecommerce', diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts index 415d98027c4f..2e42b78e5305 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/index.ts @@ -33,7 +33,7 @@ import { i18n } from '@osd/i18n'; import { getSavedObjects } from './saved_objects'; import { fieldMappings } from './field_mappings'; import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types'; -import { getSavedObjectsWithDataSource, appendDataSourceId } from '../util'; +import { addPrefixTo } from '../util'; const flightsName = i18n.translate('home.sampleData.flightsSpecTitle', { defaultMessage: 'Sample flight data', @@ -55,13 +55,11 @@ export const flightsSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), + getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), savedObjects: getSavedObjects(), - getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => - getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), dataIndices: [ { id: 'flights', diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts index 0e8eaf99d411..5c3cc9bf6861 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/index.ts @@ -33,7 +33,7 @@ import { i18n } from '@osd/i18n'; import { getSavedObjects } from './saved_objects'; import { fieldMappings } from './field_mappings'; import { SampleDatasetSchema, AppLinkSchema } from '../../lib/sample_dataset_registry_types'; -import { appendDataSourceId, getSavedObjectsWithDataSource } from '../util'; +import { addPrefixTo } from '../util'; const logsName = i18n.translate('home.sampleData.logsSpecTitle', { defaultMessage: 'Sample web logs', @@ -55,13 +55,11 @@ export const logsSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), + getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), savedObjects: getSavedObjects(), - getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => - getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), dataIndices: [ { id: 'logs', diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.ts b/src/plugins/home/server/services/sample_data/data_sets/util.ts index 46022f1c22d3..26736d503ce6 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/util.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/util.ts @@ -4,60 +4,74 @@ */ import { SavedObject } from 'opensearch-dashboards/server'; +import { cloneDeep } from 'lodash'; -export const appendDataSourceId = (id: string) => { - return (dataSourceId?: string) => (dataSourceId ? `${dataSourceId}_` + id : id); +const withPrefix = (...args: Array) => (id: string) => { + const prefix = args.filter(Boolean).join('_'); + if (prefix) { + return `${prefix}_${id}`; + } + return id; }; -export const getSavedObjectsWithDataSource = ( - saveObjectList: SavedObject[], - dataSourceId?: string, - dataSourceTitle?: string -): SavedObject[] => { - if (dataSourceId) { - return saveObjectList.map((saveObject) => { - saveObject.id = `${dataSourceId}_` + saveObject.id; - // update reference - if (saveObject.type === 'dashboard') { - saveObject.references.map((reference) => { - if (reference.id) { - reference.id = `${dataSourceId}_` + reference.id; - } - }); +export const addPrefixTo = (id: string) => (...args: Array) => { + return withPrefix(...args)(id); +}; + +const overrideSavedObjectId = (savedObject: SavedObject, idGenerator: (id: string) => string) => { + savedObject.id = idGenerator(savedObject.id); + // update reference + if (savedObject.type === 'dashboard') { + savedObject.references.map((reference) => { + if (reference.id) { + reference.id = idGenerator(reference.id); } + }); + } - // update reference - if (saveObject.type === 'visualization' || saveObject.type === 'search') { - const searchSourceString = saveObject.attributes?.kibanaSavedObjectMeta?.searchSourceJSON; - const visStateString = saveObject.attributes?.visState; + // update reference + if (savedObject.type === 'visualization' || savedObject.type === 'search') { + const searchSourceString = savedObject.attributes?.kibanaSavedObjectMeta?.searchSourceJSON; + const visStateString = savedObject.attributes?.visState; - if (searchSourceString) { - const searchSource = JSON.parse(searchSourceString); - if (searchSource.index) { - searchSource.index = `${dataSourceId}_` + searchSource.index; - saveObject.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify( - searchSource - ); - } - } + if (searchSourceString) { + const searchSource = JSON.parse(searchSourceString); + if (searchSource.index) { + searchSource.index = idGenerator(searchSource.index); + savedObject.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify( + searchSource + ); + } + } - if (visStateString) { - const visState = JSON.parse(visStateString); - const controlList = visState.params?.controls; - if (controlList) { - controlList.map((control) => { - if (control.indexPattern) { - control.indexPattern = `${dataSourceId}_` + control.indexPattern; - } - }); + if (visStateString) { + const visState = JSON.parse(visStateString); + const controlList = visState.params?.controls; + if (controlList) { + controlList.map((control) => { + if (control.indexPattern) { + control.indexPattern = idGenerator(control.indexPattern); } - saveObject.attributes.visState = JSON.stringify(visState); - } + }); } + savedObject.attributes.visState = JSON.stringify(visState); + } + } +}; + +export const getDataSourceIntegratedSavedObjects = ( + savedObjectList: SavedObject[], + dataSourceId?: string, + dataSourceTitle?: string +): SavedObject[] => { + savedObjectList = cloneDeep(savedObjectList); + if (dataSourceId) { + return savedObjectList.map((savedObject) => { + overrideSavedObjectId(savedObject, withPrefix(dataSourceId)); // update reference - if (saveObject.type === 'index-pattern') { - saveObject.references = [ + if (savedObject.type === 'index-pattern') { + savedObject.references = [ { id: `${dataSourceId}`, type: 'data-source', @@ -68,17 +82,29 @@ export const getSavedObjectsWithDataSource = ( if (dataSourceTitle) { if ( - saveObject.type === 'dashboard' || - saveObject.type === 'visualization' || - saveObject.type === 'search' + savedObject.type === 'dashboard' || + savedObject.type === 'visualization' || + savedObject.type === 'search' ) { - saveObject.attributes.title = saveObject.attributes.title + `_${dataSourceTitle}`; + savedObject.attributes.title = savedObject.attributes.title + `_${dataSourceTitle}`; } } - return saveObject; + return savedObject; }); } - return saveObjectList; + return savedObjectList; +}; + +export const getWorkspaceIntegratedSavedObjects = ( + savedObjectList: SavedObject[], + workspaceId?: string +) => { + const savedObjectListCopy = cloneDeep(savedObjectList); + + savedObjectListCopy.forEach((savedObject) => { + overrideSavedObjectId(savedObject, withPrefix(workspaceId)); + }); + return savedObjectListCopy; }; diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts index 5f6d036d6b39..33b997c4303a 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts @@ -89,7 +89,7 @@ export interface SampleDatasetSchema { // saved object id of main dashboard for sample data set overviewDashboard: string; - getDataSourceIntegratedDashboard: (dataSourceId?: string) => string; + getDashboardWithPrefix: (...args: Array) => string; appLinks: AppLinkSchema[]; // saved object id of default index-pattern for sample data set @@ -99,10 +99,6 @@ export interface SampleDatasetSchema { // OpenSearch Dashboards saved objects (index patter, visualizations, dashboard, ...) // Should provide a nice demo of OpenSearch Dashboards's functionality with the sample data set savedObjects: Array>; - getDataSourceIntegratedSavedObjects: ( - dataSourceId?: string, - dataSourceTitle?: string - ) => Array>; dataIndices: DataIndexSchema[]; status?: string | undefined; statusMsg?: unknown; diff --git a/src/plugins/home/server/services/sample_data/routes/install.test.ts b/src/plugins/home/server/services/sample_data/routes/install.test.ts index ad7b421c23d5..590edb5980ff 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.test.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.test.ts @@ -157,4 +157,67 @@ describe('sample data install route', () => { }, }); }); + + it('handler calls expected api with the given request with workspace', async () => { + const mockWorkspaceId = 'workspace'; + + const mockClient = jest.fn().mockResolvedValue(true); + + const mockSOClientGetResponse = { + saved_objects: [ + { + type: 'dashboard', + id: '12345', + namespaces: ['default'], + attributes: { title: 'dashboard' }, + }, + ], + }; + const mockSOClient = { + bulkCreate: jest.fn().mockResolvedValue(mockSOClientGetResponse), + get: jest.fn().mockResolvedValue(mockSOClientGetResponse), + }; + + const mockContext = { + core: { + opensearch: { + legacy: { + client: { callAsCurrentUser: mockClient }, + }, + }, + savedObjects: { client: mockSOClient }, + }, + }; + const mockBody = { id: 'flights' }; + const mockQuery = { workspace: mockWorkspaceId }; + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ + params: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + createInstallRoute( + mockCoreSetup.http.createRouter(), + sampleDatasets, + mockLogger, + mockUsageTracker + ); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.mock.calls[1][1].body.settings).toMatchObject({ + index: { number_of_shards: 1 }, + }); + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toMatchObject({ + body: { + opensearchIndicesCreated: { opensearch_dashboards_sample_data_flights: 13059 }, + opensearchDashboardsSavedObjectsLoaded: 20, + }, + }); + }); }); diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 279357fc1977..38fb7f3fbe21 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -39,6 +39,10 @@ import { } from '../lib/translate_timestamp'; import { loadData } from '../lib/load_data'; import { SampleDataUsageTracker } from '../usage/usage'; +import { + getDataSourceIntegratedSavedObjects, + getWorkspaceIntegratedSavedObjects, +} from '../data_sets/util'; const insertDataIntoIndex = ( dataIndexConfig: any, @@ -113,12 +117,14 @@ export function createInstallRoute( query: schema.object({ now: schema.maybe(schema.string()), data_source_id: schema.maybe(schema.string()), + workspace_id: schema.maybe(schema.string()), }), }, }, async (context, req, res) => { const { params, query } = req; const dataSourceId = query.data_source_id; + const workspaceId = query.workspace_id; const sampleDataset = sampleDatasets.find(({ id }) => id === params.id); if (!sampleDataset) { @@ -198,14 +204,22 @@ export function createInstallRoute( } let createResults; - const savedObjectsList = dataSourceId - ? sampleDataset.getDataSourceIntegratedSavedObjects(dataSourceId, dataSourceTitle) - : sampleDataset.savedObjects; + let savedObjectsList = sampleDataset.savedObjects; + if (workspaceId) { + savedObjectsList = getWorkspaceIntegratedSavedObjects(savedObjectsList, workspaceId); + } + if (dataSourceId) { + savedObjectsList = getDataSourceIntegratedSavedObjects( + savedObjectsList, + dataSourceId, + dataSourceTitle + ); + } try { createResults = await context.core.savedObjects.client.bulkCreate( savedObjectsList.map(({ version, ...savedObject }) => savedObject), - { overwrite: true } + { overwrite: true, workspaces: workspaceId ? [workspaceId] : undefined } ); } catch (err) { const errMsg = `bulkCreate failed, error: ${err.message}`; diff --git a/src/plugins/home/server/services/sample_data/routes/list.test.ts b/src/plugins/home/server/services/sample_data/routes/list.test.ts index 70201fafd06b..d8fb572da128 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.test.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.test.ts @@ -119,4 +119,109 @@ describe('sample data list route', () => { `${mockDataSourceId}_7adfa750-4c81-11e8-b3d7-01146121b73d` ); }); + + it('handler calls expected api with the given request with workspace', async () => { + const mockWorkspaceId = 'workspace'; + const mockClient = jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce({ count: 1 }); + + const mockSOClientGetResponse = { + saved_objects: [ + { + type: 'dashboard', + id: `${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d`, + namespaces: ['default'], + attributes: { title: 'dashboard' }, + }, + ], + }; + const mockSOClient = { get: jest.fn().mockResolvedValue(mockSOClientGetResponse) }; + + const mockContext = { + core: { + opensearch: { + legacy: { + client: { callAsCurrentUser: mockClient }, + }, + }, + savedObjects: { client: mockSOClient }, + }, + }; + + const mockBody = {}; + const mockQuery = { workspace_id: mockWorkspaceId }; + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + createListRoute(mockCoreSetup.http.createRouter(), sampleDatasets); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.get.mock.calls[0][1]; + + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient).toBeCalledTimes(2); + expect(mockResponse.ok).toBeCalled(); + expect(mockSOClient.get.mock.calls[0][1]).toMatch( + `${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d` + ); + }); + + it('handler calls expected api with the given request with workspace and data source', async () => { + const mockWorkspaceId = 'workspace'; + const mockDataSourceId = 'dataSource'; + const mockClient = jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce({ count: 1 }); + + const mockSOClientGetResponse = { + saved_objects: [ + { + type: 'dashboard', + id: `${mockDataSourceId}_${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d`, + namespaces: ['default'], + attributes: { title: 'dashboard' }, + }, + ], + }; + const mockSOClient = { get: jest.fn().mockResolvedValue(mockSOClientGetResponse) }; + + const mockContext = { + dataSource: { + opensearch: { + legacy: { + getClient: (id) => { + return { + callAPI: mockClient, + }; + }, + }, + }, + }, + core: { + savedObjects: { client: mockSOClient }, + }, + }; + + const mockBody = {}; + const mockQuery = { workspace_id: mockWorkspaceId, data_source_id: mockDataSourceId }; + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + createListRoute(mockCoreSetup.http.createRouter(), sampleDatasets); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.get.mock.calls[0][1]; + + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient).toBeCalledTimes(2); + expect(mockResponse.ok).toBeCalled(); + expect(mockSOClient.get.mock.calls[0][1]).toMatch( + `${mockDataSourceId}_${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d` + ); + }); }); diff --git a/src/plugins/home/server/services/sample_data/routes/list.ts b/src/plugins/home/server/services/sample_data/routes/list.ts index 5d4b036a9ead..431ab9437d55 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -42,11 +42,15 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc { path: '/api/sample_data', validate: { - query: schema.object({ data_source_id: schema.maybe(schema.string()) }), + query: schema.object({ + data_source_id: schema.maybe(schema.string()), + workspace_id: schema.maybe(schema.string()), + }), }, }, async (context, req, res) => { const dataSourceId = req.query.data_source_id; + const workspaceId = req.query.workspace_id; const registeredSampleDatasets = sampleDatasets.map((sampleDataset) => { return { @@ -56,7 +60,7 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc previewImagePath: sampleDataset.previewImagePath, darkPreviewImagePath: sampleDataset.darkPreviewImagePath, hasNewThemeImages: sampleDataset.hasNewThemeImages, - overviewDashboard: sampleDataset.getDataSourceIntegratedDashboard(dataSourceId), + overviewDashboard: sampleDataset.getDashboardWithPrefix(dataSourceId, workspaceId), appLinks: sampleDataset.appLinks, defaultIndex: sampleDataset.getDataSourceIntegratedDefaultIndex(dataSourceId), dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })), diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.test.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.test.ts index 7d9797d752cb..c12e39ba1634 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.test.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.test.ts @@ -98,4 +98,35 @@ describe('sample data uninstall route', () => { expect(mockClient).toBeCalled(); expect(mockSOClient.delete).toBeCalled(); }); + + it('handler calls expected api with the given request with workspace', async () => { + const mockWorkspaceId = 'workspace'; + const mockContext = { + core: { + opensearch: { + legacy: { + client: { callAsCurrentUser: mockClient }, + }, + }, + savedObjects: { client: mockSOClient }, + }, + }; + const mockBody = { id: 'flights' }; + const mockQuery = { workspace_id: mockWorkspaceId }; + const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ + params: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + createUninstallRoute(mockCoreSetup.http.createRouter(), sampleDatasets, mockUsageTracker); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.delete.mock.calls[0][1]; + + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient).toBeCalled(); + expect(mockSOClient.delete).toBeCalled(); + }); }); diff --git a/src/plugins/home/server/services/sample_data/routes/uninstall.ts b/src/plugins/home/server/services/sample_data/routes/uninstall.ts index d5a09ce56070..95398e63683c 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -34,6 +34,10 @@ import { IRouter } from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; import { SampleDataUsageTracker } from '../usage/usage'; +import { + getDataSourceIntegratedSavedObjects, + getWorkspaceIntegratedSavedObjects, +} from '../data_sets/util'; export function createUninstallRoute( router: IRouter, @@ -47,12 +51,14 @@ export function createUninstallRoute( params: schema.object({ id: schema.string() }), query: schema.object({ data_source_id: schema.maybe(schema.string()), + workspace_id: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { const sampleDataset = sampleDatasets.find(({ id }) => id === request.params.id); const dataSourceId = request.query.data_source_id; + const workspaceId = request.query.workspace_id; if (!sampleDataset) { return response.notFound(); @@ -78,9 +84,13 @@ export function createUninstallRoute( } } - const savedObjectsList = dataSourceId - ? sampleDataset.getDataSourceIntegratedSavedObjects(dataSourceId) - : sampleDataset.savedObjects; + let savedObjectsList = sampleDataset.savedObjects; + if (workspaceId) { + savedObjectsList = getWorkspaceIntegratedSavedObjects(savedObjectsList, workspaceId); + } + if (dataSourceId) { + savedObjectsList = getDataSourceIntegratedSavedObjects(savedObjectsList, dataSourceId); + } const deletePromises = savedObjectsList.map(({ type, id }) => context.core.savedObjects.client.delete(type, id)