From 4321cb4b3a5c312ce3d7470ee6596a7013bc7910 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 11 Mar 2024 16:53:15 +0800 Subject: [PATCH 01/17] Import sample data to workspace Signed-off-by: Lin Wang --- .../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 | 125 +++++++++++------- .../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, 357 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..aff6ebf1a822 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 @@ -5,59 +5,74 @@ import { SavedObject } from 'opensearch-dashboards/server'; -export const appendDataSourceId = (id: string) => { - return (dataSourceId?: string) => (dataSourceId ? `${dataSourceId}_` + id : id); +const cloneDeep = (payload: T): T => JSON.parse(JSON.stringify(payload)); + +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 +83,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..8990722dc8a9 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_id: 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) From d29f691901410df7586048466d624d9f5d393908 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 11 Mar 2024 17:07:30 +0800 Subject: [PATCH 02/17] Enable workspace ui plugin Signed-off-by: Lin Wang --- src/plugins/workspace/opensearch_dashboards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index 40a7eb5c3f9f..f34106ab4fed 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -2,7 +2,7 @@ "id": "workspace", "version": "opensearchDashboards", "server": true, - "ui": false, + "ui": true, "requiredPlugins": [ "savedObjects" ], From 54adb59a4365855cfb678705fc5f7e6ce82f6526 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 11 Mar 2024 18:44:38 +0800 Subject: [PATCH 03/17] Add changelog for import sample data to current workspace Signed-off-by: Lin Wang --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e31f795c37bf..3507b1fccee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Consume workspace id in saved object client ([#6014](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6014)) - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) +- [Workspace] Import sample data to current workspace([#6105](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6105)) ### 🐛 Bug Fixes From b4c0c27d8aa8c5fa0ef242fad314e57f91e331fd Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 6 May 2024 16:11:49 +0800 Subject: [PATCH 04/17] feat: register sample data as standalone app (#8) * feat: register sample data as standalone app Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: add comment Signed-off-by: SuZhou-Joe * feat: use props to pass homeLink Signed-off-by: SuZhou-Joe * feat: add unit test Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe --- src/plugins/home/common/constants.ts | 1 + .../public/application/application.test.tsx | 45 +++++++++ .../home/public/application/application.tsx | 27 ++++- .../public/application/components/home_app.js | 48 +++++++-- .../application/components/home_app.test.tsx | 60 ++++++++++++ .../components/tutorial_directory.js | 16 +-- .../components/tutorial_directory.test.tsx | 65 ++++++++++++ src/plugins/home/public/application/index.ts | 2 +- .../opensearch_dashboards_services.mock.ts | 47 +++++++++ src/plugins/home/public/plugin.test.ts | 8 ++ src/plugins/home/public/plugin.ts | 98 ++++++++++++------- 11 files changed, 365 insertions(+), 52 deletions(-) create mode 100644 src/plugins/home/public/application/application.test.tsx create mode 100644 src/plugins/home/public/application/components/home_app.test.tsx create mode 100644 src/plugins/home/public/application/components/tutorial_directory.test.tsx create mode 100644 src/plugins/home/public/application/opensearch_dashboards_services.mock.ts diff --git a/src/plugins/home/common/constants.ts b/src/plugins/home/common/constants.ts index a1fcfe265f0b..25c78c59c4ac 100644 --- a/src/plugins/home/common/constants.ts +++ b/src/plugins/home/common/constants.ts @@ -31,3 +31,4 @@ export const PLUGIN_ID = 'home'; export const HOME_APP_BASE_PATH = `/app/${PLUGIN_ID}`; export const USE_NEW_HOME_PAGE = 'home:useNewHomePage'; +export const IMPORT_SAMPLE_DATA_APP_ID = 'import_sample_data'; diff --git a/src/plugins/home/public/application/application.test.tsx b/src/plugins/home/public/application/application.test.tsx new file mode 100644 index 000000000000..95149d52cf8f --- /dev/null +++ b/src/plugins/home/public/application/application.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useRef } from 'react'; +import { render } from '@testing-library/react'; +import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks'; +import { renderImportSampleDataApp } from './application'; + +jest.mock('./components/home_app', () => ({ + HomeApp: () => 'HomeApp', + ImportSampleDataApp: () => 'ImportSampleDataApp', +})); + +const coreStartMocks = coreMock.createStart(); + +const ComponentForRender = (props: { renderFn: typeof renderImportSampleDataApp }) => { + const container = useRef(null); + const historyMock = scopedHistoryMock.create(); + historyMock.listen.mockReturnValueOnce(() => () => null); + useEffect(() => { + if (container.current) { + const destroyFn = props.renderFn(container.current, coreStartMocks, historyMock); + return () => { + destroyFn.then((res) => res()); + }; + } + }, [historyMock, props]); + + return
; +}; + +describe('renderImportSampleDataApp', () => { + it('should render ImportSampleDataApp when calling renderImportSampleDataApp', async () => { + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
+
+ ImportSampleDataApp +
+
+ `); + }); +}); diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 80b628c56b3b..bd5b3c161ff8 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -34,7 +34,7 @@ import { i18n } from '@osd/i18n'; import { ScopedHistory, CoreStart } from 'opensearch-dashboards/public'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; // @ts-ignore -import { HomeApp } from './components/home_app'; +import { HomeApp, ImportSampleDataApp } from './components/home_app'; import { getServices } from './opensearch_dashboards_services'; import './index.scss'; @@ -77,3 +77,28 @@ export const renderApp = async ( unlisten(); }; }; + +export const renderImportSampleDataApp = async ( + element: HTMLElement, + coreStart: CoreStart, + history: ScopedHistory +) => { + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + // This must be called before the app is mounted to avoid call this after the redirect to default app logic kicks in + const unlisten = history.listen((location) => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + unlisten(); + }; +}; diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 366d162f02eb..05687e09d883 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -51,6 +51,44 @@ const RedirectToDefaultApp = () => { return null; }; +const renderTutorialDirectory = (props) => { + const { addBasePath, environmentService } = getServices(); + const environment = environmentService.getEnvironment(); + const isCloudEnabled = environment.cloud; + + return ( + + ); +}; + +export function ImportSampleDataApp() { + return ( + + + + + renderTutorialDirectory({ + ...props, + // For standalone import sample data application + // home breadcrumb should not be appended as it is not a sub app of home + withoutHomeBreadCrumb: true, + }) + } + /> + + + + ); +} + export function HomeApp({ directories, solutions }) { const { savedObjectsClient, @@ -63,16 +101,6 @@ export function HomeApp({ directories, solutions }) { const environment = environmentService.getEnvironment(); const isCloudEnabled = environment.cloud; - const renderTutorialDirectory = (props) => { - return ( - - ); - }; - const renderTutorial = (props) => { return ( ({ + Home: () =>
Home
, +})); + +jest.mock('../load_tutorials', () => ({ + getTutorial: () => {}, +})); + +jest.mock('./tutorial_directory', () => ({ + TutorialDirectory: (props: { withoutHomeBreadCrumb?: boolean }) => ( +
+ ), +})); + +describe('', () => { + let currentService: ReturnType; + beforeEach(() => { + currentService = getMockedServices(); + setServices(currentService); + }); + + it('should not pass withoutHomeBreadCrumb to TutorialDirectory component', async () => { + const originalHash = window.location.hash; + const { findByTestId } = render(); + window.location.hash = '/tutorial_directory'; + const tutorialRenderResult = await findByTestId('tutorial_directory'); + expect(tutorialRenderResult.dataset.withoutHomeBreadCrumb).toEqual('false'); + + // revert to original hash + window.location.hash = originalHash; + }); +}); + +describe('', () => { + let currentService: ReturnType; + beforeEach(() => { + currentService = getMockedServices(); + setServices(currentService); + }); + + it('should pass withoutHomeBreadCrumb to TutorialDirectory component', async () => { + const { findByTestId } = render(); + const tutorialRenderResult = await findByTestId('tutorial_directory'); + expect(tutorialRenderResult.dataset.withoutHomeBreadCrumb).toEqual('true'); + }); +}); diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index a36f231b38ca..fac078abaab0 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -93,14 +93,17 @@ class TutorialDirectoryUi extends React.Component { async componentDidMount() { this._isMounted = true; - - getServices().chrome.setBreadcrumbs([ - { + const { chrome } = getServices(); + const { withoutHomeBreadCrumb } = this.props; + const breadcrumbs = [{ text: addDataTitle }]; + if (!withoutHomeBreadCrumb) { + breadcrumbs.splice(0, 0, { text: homeTitle, href: '#/', - }, - { text: addDataTitle }, - ]); + }); + } + + chrome.setBreadcrumbs(breadcrumbs); const tutorialConfigs = await getTutorials(); @@ -322,6 +325,7 @@ TutorialDirectoryUi.propTypes = { addBasePath: PropTypes.func.isRequired, openTab: PropTypes.string, isCloudEnabled: PropTypes.bool.isRequired, + withoutHomeBreadCrumb: PropTypes.bool, }; export const TutorialDirectory = injectI18n(TutorialDirectoryUi); diff --git a/src/plugins/home/public/application/components/tutorial_directory.test.tsx b/src/plugins/home/public/application/components/tutorial_directory.test.tsx new file mode 100644 index 000000000000..eacd50fc43d0 --- /dev/null +++ b/src/plugins/home/public/application/components/tutorial_directory.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { coreMock } from '../../../../../core/public/mocks'; +import { setServices } from '../opensearch_dashboards_services'; +import { getMockedServices } from '../opensearch_dashboards_services.mock'; + +const makeProps = () => { + const coreMocks = coreMock.createStart(); + return { + addBasePath: coreMocks.http.basePath.prepend, + openTab: 'foo', + isCloudEnabled: false, + }; +}; + +describe('', () => { + let currentService: ReturnType; + beforeEach(() => { + currentService = getMockedServices(); + setServices(currentService); + }); + it('should render home breadcrumbs when withoutHomeBreadCrumb is undefined', async () => { + const finalProps = makeProps(); + currentService.http.get.mockResolvedValueOnce([]); + // @ts-ignore + const { TutorialDirectory } = await import('./tutorial_directory'); + render( + + + + ); + expect(currentService.chrome.setBreadcrumbs).toBeCalledWith([ + { + href: '#/', + text: 'Home', + }, + { + text: 'Add data', + }, + ]); + }); + + it('should not render home breadcrumbs when withoutHomeBreadCrumb is true', async () => { + const finalProps = makeProps(); + currentService.http.get.mockResolvedValueOnce([]); + // @ts-ignore + const { TutorialDirectory } = await import('./tutorial_directory'); + render( + + + + ); + expect(currentService.chrome.setBreadcrumbs).toBeCalledWith([ + { + text: 'Add data', + }, + ]); + }); +}); diff --git a/src/plugins/home/public/application/index.ts b/src/plugins/home/public/application/index.ts index ba5ccc3e62fa..5bb49c2993d9 100644 --- a/src/plugins/home/public/application/index.ts +++ b/src/plugins/home/public/application/index.ts @@ -28,4 +28,4 @@ * under the License. */ -export { renderApp } from './application'; +export { renderApp, renderImportSampleDataApp } from './application'; diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.mock.ts b/src/plugins/home/public/application/opensearch_dashboards_services.mock.ts new file mode 100644 index 000000000000..b06d03cbc105 --- /dev/null +++ b/src/plugins/home/public/application/opensearch_dashboards_services.mock.ts @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock } from '../../../../core/public/mocks'; +import { urlForwardingPluginMock } from '../../../url_forwarding/public/mocks'; +import { homePluginMock } from '../mocks'; +import { + EnvironmentService, + FeatureCatalogueRegistry, + SectionTypeService, + TutorialService, +} from '../services'; +import { telemetryPluginMock } from '../../../telemetry/public/mocks'; + +export const getMockedServices = () => { + const coreMocks = coreMock.createStart(); + const urlForwarding = urlForwardingPluginMock.createStartContract(); + const homePlugin = homePluginMock.createSetupContract(); + return { + ...coreMocks, + ...homePlugin, + telemetry: telemetryPluginMock.createStartContract(), + indexPatternService: jest.fn(), + dataSource: { + dataSourceEnabled: false, + hideLocalCluster: false, + noAuthenticationTypeEnabled: false, + usernamePasswordAuthEnabled: false, + awsSigV4AuthEnabled: false, + }, + opensearchDashboardsVersion: '', + urlForwarding, + savedObjectsClient: coreMocks.savedObjects.client, + toastNotifications: coreMocks.notifications.toasts, + banners: coreMocks.overlays.banners, + trackUiMetric: jest.fn(), + getBasePath: jest.fn(), + addBasePath: jest.fn(), + environmentService: new EnvironmentService(), + tutorialService: new TutorialService(), + homeConfig: homePlugin.config, + featureCatalogue: new FeatureCatalogueRegistry(), + sectionTypes: new SectionTypeService(), + }; +}; diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index c883ff0ab771..62ab89dce847 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -96,5 +96,13 @@ describe('HomePublicPlugin', () => { expect(setup).toHaveProperty('tutorials'); expect(setup.tutorials).toHaveProperty('setVariable'); }); + + test('wires up and register applications', async () => { + const coreMocks = coreMock.createSetup(); + await new HomePublicPlugin(mockInitializerContext).setup(coreMocks, { + urlForwarding: urlForwardingPluginMock.createSetupContract(), + }); + expect(coreMocks.application.register).toBeCalledTimes(2); + }); }); }); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 6fe459570fd1..d7867959d019 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -51,13 +51,16 @@ import { SectionTypeServiceSetup, } from './services'; import { ConfigSchema } from '../config'; -import { setServices } from './application/opensearch_dashboards_services'; +import { + HomeOpenSearchDashboardsServices, + setServices, +} from './application/opensearch_dashboards_services'; import { DataPublicPluginStart } from '../../data/public'; import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; -import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; +import { PLUGIN_ID, HOME_APP_BASE_PATH, IMPORT_SAMPLE_DATA_APP_ID } from '../common/constants'; import { DataSourcePluginStart } from '../../data_source/public'; import { workWithDataSection } from './application/components/homepage/sections/work_with_data'; import { learnBasicsSection } from './application/components/homepage/sections/learn_basics'; @@ -93,43 +96,50 @@ export class HomePublicPlugin core: CoreSetup, { urlForwarding, usageCollection }: HomePluginSetupDependencies ): HomePublicPluginSetup { + const setCommonService = async ( + homeOpenSearchDashboardsServices?: Partial + ) => { + const trackUiMetric = usageCollection + ? usageCollection.reportUiStats.bind(usageCollection, 'OpenSearch_Dashboards_home') + : () => {}; + const [ + coreStart, + { telemetry, data, urlForwarding: urlForwardingStart, dataSource }, + ] = await core.getStartServices(); + setServices({ + trackUiMetric, + opensearchDashboardsVersion: this.initializerContext.env.packageInfo.version, + http: coreStart.http, + toastNotifications: core.notifications.toasts, + banners: coreStart.overlays.banners, + docLinks: coreStart.docLinks, + savedObjectsClient: coreStart.savedObjects.client, + chrome: coreStart.chrome, + application: coreStart.application, + telemetry, + uiSettings: core.uiSettings, + addBasePath: core.http.basePath.prepend, + getBasePath: core.http.basePath.get, + indexPatternService: data.indexPatterns, + environmentService: this.environmentService, + urlForwarding: urlForwardingStart, + homeConfig: this.initializerContext.config.get(), + tutorialService: this.tutorialService, + featureCatalogue: this.featuresCatalogueRegistry, + injectedMetadata: coreStart.injectedMetadata, + dataSource, + workspaces: coreStart.workspaces, + sectionTypes: this.sectionTypeService, + ...homeOpenSearchDashboardsServices, + }); + }; core.application.register({ id: PLUGIN_ID, title: 'Home', navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { - const trackUiMetric = usageCollection - ? usageCollection.reportUiStats.bind(usageCollection, 'OpenSearch_Dashboards_home') - : () => {}; - const [ - coreStart, - { telemetry, data, urlForwarding: urlForwardingStart, dataSource }, - ] = await core.getStartServices(); - setServices({ - trackUiMetric, - opensearchDashboardsVersion: this.initializerContext.env.packageInfo.version, - http: coreStart.http, - toastNotifications: core.notifications.toasts, - banners: coreStart.overlays.banners, - docLinks: coreStart.docLinks, - savedObjectsClient: coreStart.savedObjects.client, - chrome: coreStart.chrome, - application: coreStart.application, - telemetry, - uiSettings: core.uiSettings, - addBasePath: core.http.basePath.prepend, - getBasePath: core.http.basePath.get, - indexPatternService: data.indexPatterns, - environmentService: this.environmentService, - urlForwarding: urlForwardingStart, - homeConfig: this.initializerContext.config.get(), - tutorialService: this.tutorialService, - featureCatalogue: this.featuresCatalogueRegistry, - injectedMetadata: coreStart.injectedMetadata, - dataSource, - workspaces: coreStart.workspaces, - sectionTypes: this.sectionTypeService, - }); + const [coreStart] = await core.getStartServices(); + setCommonService(); coreStart.chrome.docTitle.change( i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) ); @@ -137,6 +147,26 @@ export class HomePublicPlugin return await renderApp(params.element, coreStart, params.history); }, }); + + // Register import sample data as a standalone app so that it is available inside workspace. + core.application.register({ + id: IMPORT_SAMPLE_DATA_APP_ID, + title: i18n.translate('home.tutorialDirectory.featureCatalogueTitle', { + defaultMessage: 'Add sample data', + }), + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); + setCommonService(); + coreStart.chrome.docTitle.change( + i18n.translate('home.tutorialDirectory.featureCatalogueTitle', { + defaultMessage: 'Add sample data', + }) + ); + const { renderImportSampleDataApp } = await import('./application'); + return await renderImportSampleDataApp(params.element, coreStart, params.history); + }, + }); urlForwarding.forwardApp('home', 'home'); const featureCatalogue = { ...this.featuresCatalogueRegistry.setup() }; From 9b3be8fda04e5f5c5d50c69946bac45d4cb03e2d Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 7 May 2024 17:04:52 +0800 Subject: [PATCH 05/17] Retrieve workspace id from request Signed-off-by: Lin Wang --- .../sample_data/data_sets/ecommerce/index.ts | 14 ++- .../sample_data/data_sets/flights/index.ts | 14 ++- .../sample_data/data_sets/logs/index.ts | 14 ++- .../services/sample_data/data_sets/util.ts | 94 +++++++++++-------- .../lib/sample_dataset_registry_types.ts | 9 +- .../sample_data/routes/install.test.ts | 4 +- .../services/sample_data/routes/install.ts | 26 ++--- .../services/sample_data/routes/list.test.ts | 7 +- .../services/sample_data/routes/list.ts | 15 ++- .../sample_data/routes/uninstall.test.ts | 4 +- .../services/sample_data/routes/uninstall.ts | 21 ++--- 11 files changed, 135 insertions(+), 87 deletions(-) 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 1a4ebd2a5e72..9fb6e7bedd22 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,11 @@ 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 { addPrefixTo } from '../util'; +import { + appendDataSourceId, + getSavedObjectsWithDataSource, + overwriteSavedObjectsWithWorkspaceId, +} from '../util'; const ecommerceName = i18n.translate('home.sampleData.ecommerceSpecTitle', { defaultMessage: 'Sample eCommerce orders', @@ -55,11 +59,15 @@ export const ecommerceSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/ecommerce/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), + getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), savedObjects: getSavedObjects(), + getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => + getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), + getWorkspaceIntegratedSavedObjects: (workspaceId) => + overwriteSavedObjectsWithWorkspaceId(getSavedObjects(), workspaceId), 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 2e42b78e5305..c4a6d84e752c 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,11 @@ 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 { addPrefixTo } from '../util'; +import { + appendDataSourceId, + getSavedObjectsWithDataSource, + overwriteSavedObjectsWithWorkspaceId, +} from '../util'; const flightsName = i18n.translate('home.sampleData.flightsSpecTitle', { defaultMessage: 'Sample flight data', @@ -55,11 +59,15 @@ export const flightsSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/flights/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), + getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), savedObjects: getSavedObjects(), + getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => + getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), + getWorkspaceIntegratedSavedObjects: (workspaceId) => + overwriteSavedObjectsWithWorkspaceId(getSavedObjects(), workspaceId), 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 5c3cc9bf6861..5466c9f3a22f 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,11 @@ 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 { addPrefixTo } from '../util'; +import { + appendDataSourceId, + getSavedObjectsWithDataSource, + overwriteSavedObjectsWithWorkspaceId, +} from '../util'; const logsName = i18n.translate('home.sampleData.logsSpecTitle', { defaultMessage: 'Sample web logs', @@ -55,11 +59,15 @@ export const logsSpecProvider = function (): SampleDatasetSchema { darkPreviewImagePath: '/plugins/home/assets/sample_data_resources/logs/dashboard_dark.png', hasNewThemeImages: true, overviewDashboard: DASHBOARD_ID, - getDashboardWithPrefix: addPrefixTo(DASHBOARD_ID), + getDataSourceIntegratedDashboard: appendDataSourceId(DASHBOARD_ID), appLinks: initialAppLinks, defaultIndex: DEFAULT_INDEX, - getDataSourceIntegratedDefaultIndex: addPrefixTo(DEFAULT_INDEX), + getDataSourceIntegratedDefaultIndex: appendDataSourceId(DEFAULT_INDEX), savedObjects: getSavedObjects(), + getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => + getSavedObjectsWithDataSource(getSavedObjects(), dataSourceId, dataSourceTitle), + getWorkspaceIntegratedSavedObjects: (workspaceId) => + overwriteSavedObjectsWithWorkspaceId(getSavedObjects(), workspaceId), 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 dd567a58a189..4991b02272cc 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 @@ -8,19 +8,16 @@ import { extractVegaSpecFromSavedObject, updateDataSourceNameInVegaSpec, } from '../../../../../../core/server'; +import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; -const cloneDeep = (payload: T): T => JSON.parse(JSON.stringify(payload)); - -const withPrefix = (...args: Array) => (id: string) => { - const prefix = args.filter(Boolean).join('_'); - if (prefix) { - return `${prefix}_${id}`; - } - return id; -}; - -export const addPrefixTo = (id: string) => (...args: Array) => { - return withPrefix(...args)(id); +export const appendDataSourceId = (id: string) => { + return (dataSourceId?: string, workspaceId?: string) => { + const idWithDataSource = dataSourceId ? `${dataSourceId}_` + id : id; + if (!workspaceId) { + return idWithDataSource; + } + return `${workspaceId}_${idWithDataSource}`; + }; }; const overrideSavedObjectId = (savedObject: SavedObject, idGenerator: (id: string) => string) => { @@ -64,34 +61,23 @@ const overrideSavedObjectId = (savedObject: SavedObject, idGenerator: (id: strin } }; -export const getDataSourceIntegratedSavedObjects = ( - savedObjectList: SavedObject[], +export const getSavedObjectsWithDataSource = ( + saveObjectList: SavedObject[], dataSourceId?: string, dataSourceTitle?: string ): SavedObject[] => { - savedObjectList = cloneDeep(savedObjectList); if (dataSourceId) { - return savedObjectList.map((savedObject) => { - overrideSavedObjectId(savedObject, withPrefix(dataSourceId)); - - // update reference - if (savedObject.type === 'index-pattern') { - savedObject.references = [ - { - id: `${dataSourceId}`, - type: 'data-source', - name: 'dataSource', - }, - ]; - } + const idGenerator = (id: string) => `${dataSourceId}_${id}`; + return saveObjectList.map((saveObject) => { + overrideSavedObjectId(saveObject, idGenerator); if (dataSourceTitle) { if ( - savedObject.type === 'dashboard' || - savedObject.type === 'visualization' || - savedObject.type === 'search' + saveObject.type === 'dashboard' || + saveObject.type === 'visualization' || + saveObject.type === 'search' ) { - savedObject.attributes.title = savedObject.attributes.title + `_${dataSourceTitle}`; + saveObject.attributes.title = saveObject.attributes.title + `_${dataSourceTitle}`; } if (saveObject.type === 'visualization') { @@ -120,21 +106,47 @@ export const getDataSourceIntegratedSavedObjects = ( } } - return savedObject; + return saveObject; }); } - return savedObjectList; + return saveObjectList; }; -export const getWorkspaceIntegratedSavedObjects = ( +export const overwriteSavedObjectsWithWorkspaceId = ( savedObjectList: SavedObject[], - workspaceId?: string + workspaceId: string ) => { - const savedObjectListCopy = cloneDeep(savedObjectList); - - savedObjectListCopy.forEach((savedObject) => { - overrideSavedObjectId(savedObject, withPrefix(workspaceId)); + const idGenerator = (id: string) => `${workspaceId}_${id}`; + savedObjectList.forEach((savedObject) => { + overrideSavedObjectId(savedObject, idGenerator); }); - return savedObjectListCopy; + return savedObjectList; +}; + +export const getFinalSavedObjects = ({ + dataset, + workspaceId, + dataSourceId, + dataSourceTitle, +}: { + dataset: SampleDatasetSchema; + workspaceId?: string; + dataSourceId?: string; + dataSourceTitle?: string; +}) => { + if (workspaceId && dataSourceId) { + return overwriteSavedObjectsWithWorkspaceId( + dataset.getDataSourceIntegratedSavedObjects(dataSourceId, dataSourceTitle), + workspaceId + ); + } + if (workspaceId) { + return dataset.getWorkspaceIntegratedSavedObjects(workspaceId); + } + if (dataSourceId) { + return dataset.getDataSourceIntegratedSavedObjects(dataSourceId, dataSourceTitle); + } + + return dataset.savedObjects; }; 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 33b997c4303a..625427265858 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,16 +89,21 @@ export interface SampleDatasetSchema { // saved object id of main dashboard for sample data set overviewDashboard: string; - getDashboardWithPrefix: (...args: Array) => string; + getDataSourceIntegratedDashboard: (dataSourceId?: string, workspaceId?: string) => string; appLinks: AppLinkSchema[]; // saved object id of default index-pattern for sample data set defaultIndex: string; - getDataSourceIntegratedDefaultIndex: (dataSourceId?: string) => string; + getDataSourceIntegratedDefaultIndex: (dataSourceId?: string, workspaceId?: string) => string; // 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>; + getWorkspaceIntegratedSavedObjects: (workspaceId: 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 8990722dc8a9..9e174b5a53cb 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 @@ -5,6 +5,7 @@ import { CoreSetup, RequestHandlerContext } from 'src/core/server'; import { coreMock, httpServerMock } from '../../../../../../core/server/mocks'; +import { updateWorkspaceState } from '../../../../../../core/server/utils'; import { flightsSpecProvider } from '../data_sets'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createInstallRoute } from './install'; @@ -189,11 +190,12 @@ describe('sample data install route', () => { }, }; const mockBody = { id: 'flights' }; - const mockQuery = { workspace_id: mockWorkspaceId }; + const mockQuery = {}; const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ params: mockBody, query: mockQuery, }); + updateWorkspaceState(mockRequest, { requestWorkspaceId: mockWorkspaceId }); const mockResponse = httpServerMock.createResponseFactory(); createInstallRoute( 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 38fb7f3fbe21..ef273103111e 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -30,6 +30,7 @@ import { schema } from '@osd/config-schema'; import { IRouter, LegacyCallAPIOptions, Logger } from 'src/core/server'; +import { getWorkspaceState } from '../../../../../../core/server/utils'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; import { @@ -39,10 +40,7 @@ import { } from '../lib/translate_timestamp'; import { loadData } from '../lib/load_data'; import { SampleDataUsageTracker } from '../usage/usage'; -import { - getDataSourceIntegratedSavedObjects, - getWorkspaceIntegratedSavedObjects, -} from '../data_sets/util'; +import { getFinalSavedObjects } from '../data_sets/util'; const insertDataIntoIndex = ( dataIndexConfig: any, @@ -124,7 +122,8 @@ export function createInstallRoute( async (context, req, res) => { const { params, query } = req; const dataSourceId = query.data_source_id; - const workspaceId = query.workspace_id; + const workspaceState = getWorkspaceState(req); + const workspaceId = workspaceState?.requestWorkspaceId; const sampleDataset = sampleDatasets.find(({ id }) => id === params.id); if (!sampleDataset) { @@ -204,17 +203,12 @@ export function createInstallRoute( } let createResults; - let savedObjectsList = sampleDataset.savedObjects; - if (workspaceId) { - savedObjectsList = getWorkspaceIntegratedSavedObjects(savedObjectsList, workspaceId); - } - if (dataSourceId) { - savedObjectsList = getDataSourceIntegratedSavedObjects( - savedObjectsList, - dataSourceId, - dataSourceTitle - ); - } + const savedObjectsList = getFinalSavedObjects({ + dataset: sampleDataset, + workspaceId, + dataSourceId, + dataSourceTitle, + }); try { createResults = await context.core.savedObjects.client.bulkCreate( 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 d8fb572da128..fdad9cbf2b05 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 @@ -4,6 +4,7 @@ */ import { CoreSetup, RequestHandlerContext } from 'src/core/server'; +import { updateWorkspaceState } from '../../../../../../core/server/utils'; import { coreMock, httpServerMock } from '../../../../../../core/server/mocks'; import { createListRoute } from './list'; import { flightsSpecProvider } from '../data_sets'; @@ -148,11 +149,12 @@ describe('sample data list route', () => { }; const mockBody = {}; - const mockQuery = { workspace_id: mockWorkspaceId }; + const mockQuery = {}; const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ body: mockBody, query: mockQuery, }); + updateWorkspaceState(mockRequest, { requestWorkspaceId: mockWorkspaceId }); const mockResponse = httpServerMock.createResponseFactory(); createListRoute(mockCoreSetup.http.createRouter(), sampleDatasets); @@ -204,11 +206,12 @@ describe('sample data list route', () => { }; const mockBody = {}; - const mockQuery = { workspace_id: mockWorkspaceId, data_source_id: mockDataSourceId }; + const mockQuery = { data_source_id: mockDataSourceId }; const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ body: mockBody, query: mockQuery, }); + updateWorkspaceState(mockRequest, { requestWorkspaceId: mockWorkspaceId }); const mockResponse = httpServerMock.createResponseFactory(); createListRoute(mockCoreSetup.http.createRouter(), sampleDatasets); 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 431ab9437d55..9ce7b14ff47e 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -30,6 +30,7 @@ import { IRouter } from 'src/core/server'; import { schema } from '@osd/config-schema'; +import { getWorkspaceState } from '../../../../../../core/server/utils'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; @@ -50,7 +51,8 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc }, async (context, req, res) => { const dataSourceId = req.query.data_source_id; - const workspaceId = req.query.workspace_id; + const workspaceState = getWorkspaceState(req); + const workspaceId = workspaceState?.requestWorkspaceId; const registeredSampleDatasets = sampleDatasets.map((sampleDataset) => { return { @@ -60,9 +62,15 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc previewImagePath: sampleDataset.previewImagePath, darkPreviewImagePath: sampleDataset.darkPreviewImagePath, hasNewThemeImages: sampleDataset.hasNewThemeImages, - overviewDashboard: sampleDataset.getDashboardWithPrefix(dataSourceId, workspaceId), + overviewDashboard: sampleDataset.getDataSourceIntegratedDashboard( + dataSourceId, + workspaceId + ), appLinks: sampleDataset.appLinks, - defaultIndex: sampleDataset.getDataSourceIntegratedDefaultIndex(dataSourceId), + defaultIndex: sampleDataset.getDataSourceIntegratedDefaultIndex( + dataSourceId, + workspaceId + ), dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })), status: sampleDataset.status, statusMsg: sampleDataset.statusMsg, @@ -98,6 +106,7 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc return; } } + try { await context.core.savedObjects.client.get('dashboard', sampleDataset.overviewDashboard); } catch (err) { 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 c12e39ba1634..b3fb029d1f04 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 @@ -5,6 +5,7 @@ import { CoreSetup, RequestHandlerContext } from 'src/core/server'; import { coreMock, httpServerMock } from '../../../../../../core/server/mocks'; +import { updateWorkspaceState } from '../../../../../../core/server/utils'; import { flightsSpecProvider } from '../data_sets'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createUninstallRoute } from './uninstall'; @@ -112,11 +113,12 @@ describe('sample data uninstall route', () => { }, }; const mockBody = { id: 'flights' }; - const mockQuery = { workspace_id: mockWorkspaceId }; + const mockQuery = {}; const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({ params: mockBody, query: mockQuery, }); + updateWorkspaceState(mockRequest, { requestWorkspaceId: mockWorkspaceId }); const mockResponse = httpServerMock.createResponseFactory(); createUninstallRoute(mockCoreSetup.http.createRouter(), sampleDatasets, mockUsageTracker); 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 95398e63683c..eea40e4ee7db 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -31,13 +31,11 @@ import { schema } from '@osd/config-schema'; import _ from 'lodash'; import { IRouter } from 'src/core/server'; +import { getWorkspaceState } from '../../../../../../core/server/utils'; 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'; +import { getFinalSavedObjects } from '../data_sets/util'; export function createUninstallRoute( router: IRouter, @@ -58,7 +56,8 @@ export function createUninstallRoute( 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; + const workspaceState = getWorkspaceState(request); + const workspaceId = workspaceState?.requestWorkspaceId; if (!sampleDataset) { return response.notFound(); @@ -84,13 +83,11 @@ export function createUninstallRoute( } } - let savedObjectsList = sampleDataset.savedObjects; - if (workspaceId) { - savedObjectsList = getWorkspaceIntegratedSavedObjects(savedObjectsList, workspaceId); - } - if (dataSourceId) { - savedObjectsList = getDataSourceIntegratedSavedObjects(savedObjectsList, dataSourceId); - } + const savedObjectsList = getFinalSavedObjects({ + dataset: sampleDataset, + workspaceId, + dataSourceId, + }); const deletePromises = savedObjectsList.map(({ type, id }) => context.core.savedObjects.client.delete(type, id) From 5098c095f3b782ea1985d15b18a392f19d0b324f Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 7 May 2024 20:58:23 +0800 Subject: [PATCH 06/17] Remove workspace id in query Signed-off-by: Lin Wang --- .../components/sample_data_set_cards.js | 19 +++------------ .../public/application/sample_data_client.js | 23 ++++++------------- 2 files changed, 10 insertions(+), 32 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 a98e281083b7..5d1e2b86f149 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,10 +81,7 @@ export class SampleDataSetCards extends React.Component { loadSampleDataSets = async (dataSourceId) => { let sampleDataSets; try { - sampleDataSets = await listSampleDataSets( - dataSourceId, - getServices().workspaces.currentWorkspaceId$.getValue() - ); + sampleDataSets = await listSampleDataSets(dataSourceId); } catch (fetchError) { this.toastNotifications.addDanger({ title: i18n.translate('home.sampleDataSet.unableToLoadListErrorMessage', { @@ -117,12 +114,7 @@ export class SampleDataSetCards extends React.Component { })); try { - await installSampleDataSet( - id, - targetSampleDataSet.defaultIndex, - dataSourceId, - getServices().workspaces.currentWorkspaceId$.getValue() - ); + await installSampleDataSet(id, targetSampleDataSet.defaultIndex, dataSourceId); } catch (fetchError) { if (this._isMounted) { this.setState((prevState) => ({ @@ -170,12 +162,7 @@ export class SampleDataSetCards extends React.Component { })); try { - await uninstallSampleDataSet( - id, - targetSampleDataSet.defaultIndex, - dataSourceId, - getServices().workspaces.currentWorkspaceId$.getValue() - ); + await uninstallSampleDataSet(id, targetSampleDataSet.defaultIndex, dataSourceId); } catch (fetchError) { if (this._isMounted) { this.setState((prevState) => ({ diff --git a/src/plugins/home/public/application/sample_data_client.js b/src/plugins/home/public/application/sample_data_client.js index 7334c14a7033..045736c428f6 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, workspaceId) { - const query = buildQuery(dataSourceId, workspaceId); +export async function listSampleDataSets(dataSourceId) { + const query = buildQuery(dataSourceId); return await getServices().http.get(sampleDataUrl, { query }); } -export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId, workspaceId) { - const query = buildQuery(dataSourceId, workspaceId); +export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { + const query = buildQuery(dataSourceId); await getServices().http.post(`${sampleDataUrl}/${id}`, { query }); if (getServices().uiSettings.isDefault('defaultIndex')) { @@ -52,13 +52,8 @@ export async function installSampleDataSet(id, sampleDataDefaultIndex, dataSourc clearIndexPatternsCache(); } -export async function uninstallSampleDataSet( - id, - sampleDataDefaultIndex, - dataSourceId, - workspaceId -) { - const query = buildQuery(dataSourceId, workspaceId); +export async function uninstallSampleDataSet(id, sampleDataDefaultIndex, dataSourceId) { + const query = buildQuery(dataSourceId); await getServices().http.delete(`${sampleDataUrl}/${id}`, { query }); const uiSettings = getServices().uiSettings; @@ -73,16 +68,12 @@ export async function uninstallSampleDataSet( clearIndexPatternsCache(); } -function buildQuery(dataSourceId, workspaceId) { +function buildQuery(dataSourceId) { const query = {}; if (dataSourceId) { query.data_source_id = dataSourceId; } - if (workspaceId) { - query.workspace_id = workspaceId; - } - return query; } From 58adba2ce8e83070a3f3bf9bb633a591971c42ef Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 8 May 2024 11:39:50 +0800 Subject: [PATCH 07/17] Move changelog to fragments Signed-off-by: Lin Wang --- CHANGELOG.md | 1 - changelogs/fragments/6105.yml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/6105.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index fdebf9c4d552..aa524b4ec4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,6 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Expose filterfn in datasource menu component to allow filter data sources before rendering in navigation bar ([#6113](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6113)) - [Multiple Datasource] Improves connection pooling support for AWSSigV4 clients in data sources ([#6135](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6135)) - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) -- [Workspace] Import sample data to current workspace([#6105](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6105)) - [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) - [Multiple Datasource] Add component to show single selected data source in read only mode ([#6125](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6125)) - [Multiple Datasource] Add data source aggregated view to show all compatible data sources or only show used data sources ([#6129](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6129)) diff --git a/changelogs/fragments/6105.yml b/changelogs/fragments/6105.yml new file mode 100644 index 000000000000..7266a00d2da8 --- /dev/null +++ b/changelogs/fragments/6105.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace]Import sample data to current workspace ([#6105](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6105)) From f11844fb0c7303c74bd95bacf82e872bdda92b98 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 8 May 2024 13:54:51 +0800 Subject: [PATCH 08/17] Fix sample data list unit tests Signed-off-by: Lin Wang --- .../home/server/services/sample_data/routes/list.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fdad9cbf2b05..445cc1e18a05 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 @@ -180,7 +180,7 @@ describe('sample data list route', () => { saved_objects: [ { type: 'dashboard', - id: `${mockDataSourceId}_${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d`, + id: `${mockWorkspaceId}_${mockDataSourceId}_7adfa750-4c81-11e8-b3d7-01146121b73d`, namespaces: ['default'], attributes: { title: 'dashboard' }, }, @@ -224,7 +224,7 @@ describe('sample data list route', () => { expect(mockClient).toBeCalledTimes(2); expect(mockResponse.ok).toBeCalled(); expect(mockSOClient.get.mock.calls[0][1]).toMatch( - `${mockDataSourceId}_${mockWorkspaceId}_7adfa750-4c81-11e8-b3d7-01146121b73d` + `${mockWorkspaceId}_${mockDataSourceId}_7adfa750-4c81-11e8-b3d7-01146121b73d` ); }); }); From efc5b1c06e72fa02b60323fc6ad7fe02a2443e94 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 8 May 2024 14:45:46 +0800 Subject: [PATCH 09/17] Remove no need workspaces deps Signed-off-by: Lin Wang --- .../home/public/application/opensearch_dashboards_services.ts | 2 -- src/plugins/home/public/plugin.ts | 1 - src/plugins/home/server/services/sample_data/routes/install.ts | 3 +-- src/plugins/home/server/services/sample_data/routes/list.ts | 2 -- .../home/server/services/sample_data/routes/uninstall.ts | 1 - 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/plugins/home/public/application/opensearch_dashboards_services.ts b/src/plugins/home/public/application/opensearch_dashboards_services.ts index 02dc4b2b84b4..727cb03c10ab 100644 --- a/src/plugins/home/public/application/opensearch_dashboards_services.ts +++ b/src/plugins/home/public/application/opensearch_dashboards_services.ts @@ -37,7 +37,6 @@ import { SavedObjectsClientContract, IUiSettingsClient, ApplicationStart, - WorkspacesStart, } from 'opensearch-dashboards/public'; import { UiStatsMetricType } from '@osd/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; @@ -75,7 +74,6 @@ export interface HomeOpenSearchDashboardsServices { getBranding: () => HomePluginBranding; }; dataSource?: DataSourcePluginStart; - workspaces: WorkspacesStart; sectionTypes: SectionTypeService; } diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index b4c31122debd..06a8ac9a9cfb 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -128,7 +128,6 @@ export class HomePublicPlugin featureCatalogue: this.featuresCatalogueRegistry, injectedMetadata: coreStart.injectedMetadata, dataSource, - workspaces: coreStart.workspaces, sectionTypes: this.sectionTypeService, ...homeOpenSearchDashboardsServices, }); 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 ef273103111e..8134f8099352 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -115,7 +115,6 @@ export function createInstallRoute( query: schema.object({ now: schema.maybe(schema.string()), data_source_id: schema.maybe(schema.string()), - workspace_id: schema.maybe(schema.string()), }), }, }, @@ -213,7 +212,7 @@ export function createInstallRoute( try { createResults = await context.core.savedObjects.client.bulkCreate( savedObjectsList.map(({ version, ...savedObject }) => savedObject), - { overwrite: true, workspaces: workspaceId ? [workspaceId] : undefined } + { overwrite: true } ); } catch (err) { const errMsg = `bulkCreate failed, error: ${err.message}`; 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 9ce7b14ff47e..9a3f1acbead2 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -45,7 +45,6 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc validate: { query: schema.object({ data_source_id: schema.maybe(schema.string()), - workspace_id: schema.maybe(schema.string()), }), }, }, @@ -106,7 +105,6 @@ export const createListRoute = (router: IRouter, sampleDatasets: SampleDatasetSc return; } } - try { await context.core.savedObjects.client.get('dashboard', sampleDataset.overviewDashboard); } catch (err) { 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 eea40e4ee7db..472bc6bc4aad 100644 --- a/src/plugins/home/server/services/sample_data/routes/uninstall.ts +++ b/src/plugins/home/server/services/sample_data/routes/uninstall.ts @@ -49,7 +49,6 @@ 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()), }), }, }, From 8b67b94386ddebc4f5e363f225903a228fbe88c0 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 8 May 2024 15:46:29 +0800 Subject: [PATCH 10/17] Remove manual created changelogs Signed-off-by: Lin Wang --- changelogs/fragments/6105.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 changelogs/fragments/6105.yml diff --git a/changelogs/fragments/6105.yml b/changelogs/fragments/6105.yml deleted file mode 100644 index 7266a00d2da8..000000000000 --- a/changelogs/fragments/6105.yml +++ /dev/null @@ -1,2 +0,0 @@ -feat: -- [Workspace]Import sample data to current workspace ([#6105](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6105)) From e87cb729792b2ec487191753585229741b3ee9ee Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 08:10:30 +0000 Subject: [PATCH 11/17] Changeset file for PR #6105 created/updated --- changelogs/fragments/6105.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/6105.yml diff --git a/changelogs/fragments/6105.yml b/changelogs/fragments/6105.yml new file mode 100644 index 000000000000..30038aad59c3 --- /dev/null +++ b/changelogs/fragments/6105.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace]Import sample data to current workspace ([#6105](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6105)) \ No newline at end of file From 799f47522d05951ba3f63f11debd32ab162d721c Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 9 May 2024 15:56:52 +0800 Subject: [PATCH 12/17] Enable sample data in workspace overview page (#9) * enable sample data in workspace overview page Signed-off-by: Hailong Cui * add comments for empty id Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui --- .../workspace_overview.test.tsx.snap | 54 +++++++++++++++++++ .../all_get_started_cards.ts | 4 +- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_overview/__snapshots__/workspace_overview.test.tsx.snap b/src/plugins/workspace/public/components/workspace_overview/__snapshots__/workspace_overview.test.tsx.snap index 357f190534a3..650f0775b8e4 100644 --- a/src/plugins/workspace/public/components/workspace_overview/__snapshots__/workspace_overview.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_overview/__snapshots__/workspace_overview.test.tsx.snap @@ -50,6 +50,60 @@ exports[`WorkspaceOverview render workspace overview page normally 1`] = ` class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" data-test-subj="workspaceGetStartCards" > +
+
+
+ + + + +
+ +
+
diff --git a/src/plugins/workspace/public/components/workspace_overview/all_get_started_cards.ts b/src/plugins/workspace/public/components/workspace_overview/all_get_started_cards.ts index c9986b00f5c1..ad7dcad86bb8 100644 --- a/src/plugins/workspace/public/components/workspace_overview/all_get_started_cards.ts +++ b/src/plugins/workspace/public/components/workspace_overview/all_get_started_cards.ts @@ -12,10 +12,10 @@ import { WORKSPACE_APP_CATEGORIES } from '../../../common/constants'; export const getStartCards: GetStartCard[] = [ // getStarted { - id: 'home', + id: '', // set id as empty so that it will always show up featureDescription: 'Discover pre-loaded datasets before adding your own.', featureName: 'Sample Datasets', - link: '/app/home#/tutorial_directory', + link: '/app/import_sample_data', category: WORKSPACE_APP_CATEGORIES.getStarted, }, { From 5767ffc5c3a1024b2f21e1dc25cb0a839342fc46 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 9 May 2024 17:32:30 +0800 Subject: [PATCH 13/17] Add unit tests for getFinalSavedObjects in data sets util file Signed-off-by: Lin Wang --- .../sample_data/data_sets/util.test.ts | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.test.ts b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts index 56f2f15bca64..0cefac395b9c 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/util.test.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { getSavedObjectsWithDataSource } from './util'; +import { getSavedObjectsWithDataSource, getFinalSavedObjects, appendDataSourceId } from './util'; import { SavedObject, updateDataSourceNameInVegaSpec } from '../../../../../../core/server'; import visualizationObjects from './test_utils/visualization_objects.json'; @@ -63,3 +63,100 @@ describe('getSavedObjectsWithDataSource()', () => { expect(updatedVegaVisualizationsFields).toEqual(expect.arrayContaining(expectedUpdatedFields)); }); }); + +describe('getFinalSavedObjects()', () => { + const savedObjects = [ + { id: 'saved-object-1', type: 'test', attributes: { title: 'Saved object 1' }, references: [] }, + ]; + const generateTestDataSet = () => { + return { + id: 'foo', + name: 'Foo', + description: 'A test sample data set', + previewImagePath: '', + darkPreviewImagePath: '', + overviewDashboard: '', + getDataSourceIntegratedDashboard: () => '', + appLinks: [], + defaultIndex: '', + getDataSourceIntegratedDefaultIndex: () => '', + savedObjects, + getDataSourceIntegratedSavedObjects: (dataSourceId?: string, dataSourceTitle?: string) => + savedObjects.map((item) => ({ + ...item, + ...(dataSourceId ? { id: `${dataSourceId}_${item.id}` } : {}), + attributes: { + ...item.attributes, + title: dataSourceTitle + ? `${item.attributes.title}_${dataSourceTitle}` + : item.attributes.title, + }, + })), + getWorkspaceIntegratedSavedObjects: (workspaceId?: string) => + savedObjects.map((item) => ({ + ...item, + ...(workspaceId ? { id: `${workspaceId}_${item.id}` } : {}), + })), + dataIndices: [], + }; + }; + it('should return consistent saved object id and title when workspace id and data source provided', () => { + expect( + getFinalSavedObjects({ + dataset: generateTestDataSet(), + workspaceId: 'workspace-1', + dataSourceId: 'datasource-1', + dataSourceTitle: 'data source 1', + }) + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: `workspace-1_datasource-1_saved-object-1`, + attributes: expect.objectContaining({ + title: 'Saved object 1_data source 1', + }), + }), + ]) + ); + }); + it('should return consistent saved object id when workspace id', () => { + expect( + getFinalSavedObjects({ + dataset: generateTestDataSet(), + workspaceId: 'workspace-1', + }) + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: `workspace-1_saved-object-1`, + }), + ]) + ); + }); + it('should return consistent saved object id and title when data source id and title', () => { + expect( + getFinalSavedObjects({ + dataset: generateTestDataSet(), + dataSourceId: 'data-source-1', + dataSourceTitle: 'data source 1', + }) + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: `data-source-1_saved-object-1`, + attributes: expect.objectContaining({ + title: 'Saved object 1_data source 1', + }), + }), + ]) + ); + }); + it('should return original saved objects when no workspace and data source provided', () => { + const dataset = generateTestDataSet(); + expect( + getFinalSavedObjects({ + dataset, + }) + ).toBe(dataset.savedObjects); + }); +}); From dd998f3546290ee35c75aa61c8ddc8dac25a8c23 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 13 May 2024 11:51:15 +0800 Subject: [PATCH 14/17] Add unit tests for renderImportSampleDataApp destroy Signed-off-by: Lin Wang --- .../public/application/application.test.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/plugins/home/public/application/application.test.tsx b/src/plugins/home/public/application/application.test.tsx index 95149d52cf8f..fb852bb2a387 100644 --- a/src/plugins/home/public/application/application.test.tsx +++ b/src/plugins/home/public/application/application.test.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect, useRef } from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks'; import { renderImportSampleDataApp } from './application'; @@ -15,9 +15,12 @@ jest.mock('./components/home_app', () => ({ const coreStartMocks = coreMock.createStart(); -const ComponentForRender = (props: { renderFn: typeof renderImportSampleDataApp }) => { +const ComponentForRender = (props: { + renderFn: typeof renderImportSampleDataApp; + historyMock?: ReturnType; +}) => { const container = useRef(null); - const historyMock = scopedHistoryMock.create(); + const historyMock = props.historyMock || scopedHistoryMock.create(); historyMock.listen.mockReturnValueOnce(() => () => null); useEffect(() => { if (container.current) { @@ -42,4 +45,16 @@ describe('renderImportSampleDataApp', () => {
`); }); + it('should call unlisten history after destroy', async () => { + const historyMock = scopedHistoryMock.create(); + const unlistenMock = jest.fn(() => null); + historyMock.listen.mockImplementationOnce(() => unlistenMock); + const { unmount } = render( + + ); + unmount(); + await waitFor(() => { + expect(unlistenMock).toHaveBeenCalled(); + }); + }); }); From 28d1f748cb2ba1283c2c1489c14664c04a46b901 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 17 May 2024 23:12:42 +0800 Subject: [PATCH 15/17] Address PR comments Signed-off-by: Lin Wang --- src/plugins/home/public/application/application.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/home/public/application/application.test.tsx b/src/plugins/home/public/application/application.test.tsx index fb852bb2a387..46974ac6e5d9 100644 --- a/src/plugins/home/public/application/application.test.tsx +++ b/src/plugins/home/public/application/application.test.tsx @@ -45,7 +45,7 @@ describe('renderImportSampleDataApp', () => {
`); }); - it('should call unlisten history after destroy', async () => { + it('should clean up history listeners after unmount', async () => { const historyMock = scopedHistoryMock.create(); const unlistenMock = jest.fn(() => null); historyMock.listen.mockImplementationOnce(() => unlistenMock); From 5f8f9f4a88b59dec35bf2657d206f7051f273f7a Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 21 May 2024 17:03:39 +0800 Subject: [PATCH 16/17] Remove history listen in renderImportSampleDataApp Signed-off-by: Lin Wang --- .../public/application/application.test.tsx | 27 ++++--------------- .../home/public/application/application.tsx | 14 +--------- src/plugins/home/public/plugin.ts | 2 +- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/src/plugins/home/public/application/application.test.tsx b/src/plugins/home/public/application/application.test.tsx index 46974ac6e5d9..2bf8eb66f583 100644 --- a/src/plugins/home/public/application/application.test.tsx +++ b/src/plugins/home/public/application/application.test.tsx @@ -4,8 +4,8 @@ */ import React, { useEffect, useRef } from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks'; +import { render } from '@testing-library/react'; +import { coreMock } from '../../../../core/public/mocks'; import { renderImportSampleDataApp } from './application'; jest.mock('./components/home_app', () => ({ @@ -15,21 +15,16 @@ jest.mock('./components/home_app', () => ({ const coreStartMocks = coreMock.createStart(); -const ComponentForRender = (props: { - renderFn: typeof renderImportSampleDataApp; - historyMock?: ReturnType; -}) => { +const ComponentForRender = (props: { renderFn: typeof renderImportSampleDataApp }) => { const container = useRef(null); - const historyMock = props.historyMock || scopedHistoryMock.create(); - historyMock.listen.mockReturnValueOnce(() => () => null); useEffect(() => { if (container.current) { - const destroyFn = props.renderFn(container.current, coreStartMocks, historyMock); + const destroyFn = props.renderFn(container.current, coreStartMocks); return () => { destroyFn.then((res) => res()); }; } - }, [historyMock, props]); + }, [props]); return
; }; @@ -45,16 +40,4 @@ describe('renderImportSampleDataApp', () => {
`); }); - it('should clean up history listeners after unmount', async () => { - const historyMock = scopedHistoryMock.create(); - const unlistenMock = jest.fn(() => null); - historyMock.listen.mockImplementationOnce(() => unlistenMock); - const { unmount } = render( - - ); - unmount(); - await waitFor(() => { - expect(unlistenMock).toHaveBeenCalled(); - }); - }); }); diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index bd5b3c161ff8..7b383b53ae92 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -78,18 +78,7 @@ export const renderApp = async ( }; }; -export const renderImportSampleDataApp = async ( - element: HTMLElement, - coreStart: CoreStart, - history: ScopedHistory -) => { - // dispatch synthetic hash change event to update hash history objects - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - // This must be called before the app is mounted to avoid call this after the redirect to default app logic kicks in - const unlisten = history.listen((location) => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }); - +export const renderImportSampleDataApp = async (element: HTMLElement, coreStart: CoreStart) => { render( @@ -99,6 +88,5 @@ export const renderImportSampleDataApp = async ( return () => { unmountComponentAtNode(element); - unlisten(); }; }; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 06a8ac9a9cfb..a9e4cb263e88 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -164,7 +164,7 @@ export class HomePublicPlugin }) ); const { renderImportSampleDataApp } = await import('./application'); - return await renderImportSampleDataApp(params.element, coreStart, params.history); + return await renderImportSampleDataApp(params.element, coreStart); }, }); urlForwarding.forwardApp('home', 'home'); From 4d560c6723573a75694a92d97760006486b23c74 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 22 May 2024 23:29:32 +0800 Subject: [PATCH 17/17] Remove Route for workspace import sample data entry point Signed-off-by: Lin Wang --- .../public/application/components/home_app.js | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 05687e09d883..4febddb9148d 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -69,22 +69,13 @@ const renderTutorialDirectory = (props) => { export function ImportSampleDataApp() { return ( - - - - renderTutorialDirectory({ - ...props, - // For standalone import sample data application - // home breadcrumb should not be appended as it is not a sub app of home - withoutHomeBreadCrumb: true, - }) - } - /> - - + {renderTutorialDirectory({ + // Pass a fixed tab to avoid TutorialDirectory missing openTab property + match: { + params: { tab: 'sampleData' }, + }, + withoutHomeBreadCrumb: true, + })} ); }