Skip to content

Commit

Permalink
[Workspace] [Data Source] feat: support workspace level default data …
Browse files Browse the repository at this point in the history
…source (#7188) (#7281)

* feat: support set default ds in workspace

Signed-off-by: tygao <tygao@amazon.com>

* add updateWorkspaceState

Signed-off-by: tygao <tygao@amazon.com>

* Changeset file for PR #7188 created/updated

* update workspace plugin test

Signed-off-by: tygao <tygao@amazon.com>

* add reset workspace state and update tests

Signed-off-by: tygao <tygao@amazon.com>

* update and add tests for workspace client

Signed-off-by: tygao <tygao@amazon.com>

* clear default data source if no data source left and seperate wrapper

Signed-off-by: tygao <tygao@amazon.com>

* add try catch and add tests

Signed-off-by: tygao <tygao@amazon.com>

* disable DSM in workspace

Signed-off-by: tygao <tygao@amazon.com>

---------

Signed-off-by: tygao <tygao@amazon.com>
Signed-off-by: Tianyu Gao <tygao@amazon.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Co-authored-by: Yulong Ruan <ruanyl@amazon.com>
(cherry picked from commit e1d5096)
  • Loading branch information
raintygao committed Jul 17, 2024
1 parent e751b69 commit 42140ca
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 24 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7188.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Support workspace level default data source ([#7188](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7188))
1 change: 1 addition & 0 deletions src/plugins/data_source_management/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

export const PLUGIN_ID = 'dataSourceManagement';
export const PLUGIN_NAME = 'Data sources';
export const DEFAULT_DATA_SOURCE_UI_SETTINGS_ID = 'defaultDataSource';
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export const CONNECT_DATASOURCES_MESSAGE = 'Connect your data sources to get sta
export const NO_COMPATIBLE_DATASOURCES_MESSAGE = 'No compatible data sources are available.';
export const ADD_COMPATIBLE_DATASOURCES_MESSAGE = 'Add a compatible data source.';

export const DEFAULT_DATA_SOURCE_UI_SETTINGS_ID = 'defaultDataSource';
export { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../common';
1 change: 0 additions & 1 deletion src/plugins/data_source_management/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export interface DataSourceManagementPluginStart {
getAuthenticationMethodRegistry: () => IAuthenticationMethodRegistry;
}

// src/plugins/workspace/public/plugin.ts Workspace depends on this ID and hard code to avoid adding dependency on DSM bundle.
export const DSM_APP_ID = 'dataSources';

export class DataSourceManagementPlugin
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
const globalConfig = await this.globalConfig$.pipe(first()).toPromise();
const isPermissionControlEnabled = globalConfig.savedObjects.permission.enabled === true;

this.client = new WorkspaceClient(core);
this.client = new WorkspaceClient(core, this.logger);

await this.client.setup(core);

Expand Down Expand Up @@ -176,6 +176,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
this.logger.debug('Starting Workspace service');
this.permissionControl?.setup(core.savedObjects.getScopedClient, core.http.auth);
this.client?.setSavedObjects(core.savedObjects);
this.client?.setUiSettings(core.uiSettings);
this.workspaceConflictControl?.setSerializer(core.savedObjects.createSerializer());
this.workspaceSavedObjectsClientWrapper?.setScopedClient(core.savedObjects.getScopedClient);
this.workspaceUiSettingsClientWrapper?.setScopedClient(core.savedObjects.getScopedClient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { loggerMock } from '@osd/logging/target/mocks';
import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks';
import { WorkspaceUiSettingsClientWrapper } from './workspace_ui_settings_client_wrapper';
import { WORKSPACE_TYPE } from '../../../../core/server';
import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../../data_source_management/common';

import * as utils from '../../../../core/server/utils';

Expand All @@ -28,6 +29,7 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
type: 'config',
attributes: {
defaultIndex: 'default-index-global',
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'default-ds-global',
},
});
} else if (type === WORKSPACE_TYPE) {
Expand Down Expand Up @@ -59,7 +61,7 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
};
};

it('should return workspace ui settings if in a workspace', async () => {
it('should return workspace ui settings and should return workspace default data source and not extend global if in a workspace', async () => {
// Currently in a workspace
jest.spyOn(utils, 'getWorkspaceState').mockReturnValue({ requestWorkspaceId: 'workspace-id' });

Expand All @@ -72,6 +74,7 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
type: 'config',
attributes: {
defaultIndex: 'default-index-workspace',
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: undefined,
},
});
});
Expand All @@ -89,6 +92,7 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
type: 'config',
attributes: {
defaultIndex: 'default-index-global',
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'default-ds-global',
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '../../../../core/server';
import { WORKSPACE_UI_SETTINGS_CLIENT_WRAPPER_ID } from '../../common/constants';
import { Logger } from '../../../../core/server';
import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../../data_source_management/common';

/**
* This saved object client wrapper offers methods to get and update UI settings considering
Expand Down Expand Up @@ -73,9 +74,14 @@ export class WorkspaceUiSettingsClientWrapper {
this.logger.error(`Unable to get workspaceObject with id: ${requestWorkspaceId}`);
}

const workspaceLevelDefaultDS =
workspaceObject?.attributes?.uiSettings?.[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID];

configObject.attributes = {
...configObject.attributes,
...(workspaceObject ? workspaceObject.attributes.uiSettings : {}),
// Workspace level default data source value should not extend global UIsettings value.
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: workspaceLevelDefaultDS,
};

return configObject as SavedObject<T>;
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/workspace/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
WorkspaceAttribute,
SavedObjectsServiceStart,
Permissions,
UiSettingsServiceStart,
} from '../../../core/server';

export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute {
Expand Down Expand Up @@ -48,6 +49,13 @@ export interface IWorkspaceClientImpl {
* @public
*/
setSavedObjects(savedObjects: SavedObjectsServiceStart): void;
/**
* Set ui settings client that will be used inside the workspace client.
* @param uiSettings {@link UiSettingsServiceStart}
* @returns void
* @public
*/
setUiSettings(uiSettings: UiSettingsServiceStart): void;
/**
* Create a workspace
* @param requestDetail {@link IRequestDetail}
Expand Down
51 changes: 51 additions & 0 deletions src/plugins/workspace/server/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
httpServerMock,
httpServiceMock,
savedObjectsClientMock,
uiSettingsServiceMock,
} from '../../../core/server/mocks';
import {
generateRandomId,
Expand All @@ -16,9 +17,11 @@ import {
updateDashboardAdminStateForRequest,
transferCurrentUserInPermissions,
getDataSourcesList,
checkAndSetDefaultDataSource,
} from './utils';
import { getWorkspaceState } from '../../../core/server/utils';
import { Observable, of } from 'rxjs';
import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../data_source_management/common';

describe('workspace utils', () => {
const mockAuth = httpServiceMock.createAuth();
Expand Down Expand Up @@ -205,4 +208,52 @@ describe('workspace utils', () => {
const result = await getDataSourcesList(savedObjectsClient, []);
expect(result).toEqual([]);
});

it('should set first data sources as default when not need check', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createStartContract();
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const dataSources = ['id1', 'id2'];
await checkAndSetDefaultDataSource(uiSettingsClient, dataSources, false);
expect(uiSettingsClient.set).toHaveBeenCalledWith(
DEFAULT_DATA_SOURCE_UI_SETTINGS_ID,
dataSources[0]
);
});

it('should not set default data source after checking if not needed', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createStartContract();
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const dataSources = ['id1', 'id2'];
uiSettingsClient.get = jest.fn().mockResolvedValue(dataSources[0]);
await checkAndSetDefaultDataSource(uiSettingsClient, dataSources, true);
expect(uiSettingsClient.set).not.toBeCalled();
});

it('should check then set first data sources as default if needed when checking', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createStartContract();
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const dataSources = ['id1', 'id2'];
uiSettingsClient.get = jest.fn().mockResolvedValue('');
await checkAndSetDefaultDataSource(uiSettingsClient, dataSources, true);
expect(uiSettingsClient.set).toHaveBeenCalledWith(
DEFAULT_DATA_SOURCE_UI_SETTINGS_ID,
dataSources[0]
);
});

it('should clear default data source if there is no new data source', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createStartContract();
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const dataSources: string[] = [];
uiSettingsClient.get = jest.fn().mockResolvedValue('');
await checkAndSetDefaultDataSource(uiSettingsClient, dataSources, true);
expect(uiSettingsClient.set).toHaveBeenCalledWith(
DEFAULT_DATA_SOURCE_UI_SETTINGS_ID,
undefined
);
});
});
24 changes: 24 additions & 0 deletions src/plugins/workspace/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import {
SharedGlobalConfig,
Permissions,
SavedObjectsClientContract,
IUiSettingsClient,
} from '../../../core/server';
import { AuthInfo } from './types';
import { updateWorkspaceState } from '../../../core/server/utils';
import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../data_source_management/common';
import { CURRENT_USER_PLACEHOLDER } from '../common/constants';

/**
Expand Down Expand Up @@ -136,3 +138,25 @@ export const getDataSourcesList = (client: SavedObjectsClientContract, workspace
}
});
};

export const checkAndSetDefaultDataSource = async (
uiSettingsClient: IUiSettingsClient,
dataSources: string[],
needCheck: boolean
) => {
if (dataSources?.length > 0) {
if (!needCheck) {
// Create# Will set first data source as default data source.
await uiSettingsClient.set(DEFAULT_DATA_SOURCE_UI_SETTINGS_ID, dataSources[0]);
} else {
// Update will check if default DS still exists.
const defaultDSId = (await uiSettingsClient.get(DEFAULT_DATA_SOURCE_UI_SETTINGS_ID)) ?? '';
if (!dataSources.includes(defaultDSId)) {
await uiSettingsClient.set(DEFAULT_DATA_SOURCE_UI_SETTINGS_ID, dataSources[0]);
}
}
} else {
// If there is no data source left, clear workspace level default data source.
await uiSettingsClient.set(DEFAULT_DATA_SOURCE_UI_SETTINGS_ID, undefined);
}
};
80 changes: 63 additions & 17 deletions src/plugins/workspace/server/workspace_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@

import { WorkspaceClient } from './workspace_client';

import { coreMock, httpServerMock } from '../../../core/server/mocks';
import {
coreMock,
httpServerMock,
uiSettingsServiceMock,
loggingSystemMock,
} from '../../../core/server/mocks';
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../data_source/common';
import { SavedObjectsServiceStart } from '../../../core/server';
import { SavedObjectsServiceStart, SavedObjectsClientContract } from '../../../core/server';
import { IRequestDetail } from './types';

const coreSetup = coreMock.createSetup();

const mockWorkspaceId = 'workspace_id';
const mockWorkspaceName = 'workspace_name';
const mockCheckAndSetDefaultDataSource = jest.fn();
const logger = loggingSystemMock.create().get();

jest.mock('./utils', () => ({
generateRandomId: () => mockWorkspaceId,
Expand All @@ -25,6 +32,7 @@ jest.mock('./utils', () => ({
id: 'id2',
},
]),
checkAndSetDefaultDataSource: (...args) => mockCheckAndSetDefaultDataSource(...args),
}));

describe('#WorkspaceClient', () => {
Expand All @@ -35,28 +43,32 @@ describe('#WorkspaceClient', () => {
const find = jest.fn();
const addToWorkspaces = jest.fn();
const deleteFromWorkspaces = jest.fn();
const savedObjectClient = ({
find,
addToWorkspaces,
deleteFromWorkspaces,
create: jest.fn(),
get: jest.fn().mockResolvedValue({
attributes: {
name: mockWorkspaceName,
},
}),
} as unknown) as SavedObjectsClientContract;
const savedObjects = ({
...coreSetup.savedObjects,
getScopedClient: () => ({
find,
addToWorkspaces,
deleteFromWorkspaces,
get: jest.fn().mockResolvedValue({
attributes: {
name: mockWorkspaceName,
},
}),
}),
getScopedClient: () => savedObjectClient,
} as unknown) as SavedObjectsServiceStart;

const uiSettings = uiSettingsServiceMock.createStartContract();

const mockRequestDetail = ({
request: httpServerMock.createOpenSearchDashboardsRequest(),
context: coreMock.createRequestHandlerContext(),
logger: {},
} as unknown) as IRequestDetail;

it('create# should not call addToWorkspaces if no data sources passed', async () => {
const client = new WorkspaceClient(coreSetup);
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);

Expand All @@ -69,7 +81,7 @@ describe('#WorkspaceClient', () => {
});

it('create# should call addToWorkspaces with passed data sources normally', async () => {
const client = new WorkspaceClient(coreSetup);
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);

Expand All @@ -84,8 +96,25 @@ describe('#WorkspaceClient', () => {
]);
});

it('create# should call set default data source after creating', async () => {
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);
client?.setUiSettings(uiSettings);

await client.create(mockRequestDetail, {
name: mockWorkspaceName,
permissions: {},
dataSources: ['id1'],
});

const uiSettingsClient = uiSettings.asScopedToClient(savedObjectClient);

expect(mockCheckAndSetDefaultDataSource).toHaveBeenCalledWith(uiSettingsClient, ['id1'], false);
});

it('update# should not call addToWorkspaces if no new data sources added', async () => {
const client = new WorkspaceClient(coreSetup);
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);

Expand All @@ -99,7 +128,7 @@ describe('#WorkspaceClient', () => {
});

it('update# should call deleteFromWorkspaces if there is data source to be removed', async () => {
const client = new WorkspaceClient(coreSetup);
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);

Expand All @@ -117,7 +146,7 @@ describe('#WorkspaceClient', () => {
]);
});
it('update# should calculate data sources to be added and to be removed', async () => {
const client = new WorkspaceClient(coreSetup);
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);

Expand All @@ -134,4 +163,21 @@ describe('#WorkspaceClient', () => {
mockWorkspaceId,
]);
});

it('update# should call set default data source with check after updating', async () => {
const client = new WorkspaceClient(coreSetup, logger);
await client.setup(coreSetup);
client?.setSavedObjects(savedObjects);
client?.setUiSettings(uiSettings);

await client.update(mockRequestDetail, mockWorkspaceId, {
name: mockWorkspaceName,
permissions: {},
dataSources: ['id1'],
});

const uiSettingsClient = uiSettings.asScopedToClient(savedObjectClient);

expect(mockCheckAndSetDefaultDataSource).toHaveBeenCalledWith(uiSettingsClient, ['id1'], true);
});
});
Loading

0 comments on commit 42140ca

Please sign in to comment.