Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workspace] [Data Source] feat: support workspace level default data source #7188

Merged
merged 11 commits into from
Jul 16, 2024
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 @@ -19,8 +19,6 @@ 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 const OPENSEARCH_DOCUMENTATION_URL =
'https://opensearch.org/docs/latest/dashboards/management/data-sources/';

Expand All @@ -46,3 +44,5 @@ export const UrlToDatasourceType: { [key: string]: DatasourceType } = {
};

export type AuthMethod = 'noauth' | 'basicauth' | 'awssigv4';

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);
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
}
};
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
Loading