Skip to content

Commit

Permalink
[Workspace][Data Source] Support data source assignment in workspace (#…
Browse files Browse the repository at this point in the history
…7101) (#7173)

* feat: add data source assign



* update workspace update



* update workspace update test



* Changeset file for PR #7101 created/updated

* update name



* update repository test



* update integration test



* update integration test



* add test cases for utils



* update utils test



* add tests for saved object client



* update and add tests for workspace client



* use overwrite in deleteFromWorkspaces and update find



* update saved ojects client test



* use Promise.all and updates comment and type



* add rescriction for title field to make it optional



* use string array to send selected data sources



---------



(cherry picked from commit 5a4aba9)

Signed-off-by: tygao <tygao@amazon.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 9, 2024
1 parent c74f52d commit 5d33328
Show file tree
Hide file tree
Showing 32 changed files with 1,105 additions and 102 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7101.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Support data source assignment in workspace. ([#7101](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7101))
30 changes: 30 additions & 0 deletions src/core/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,8 @@ describe('SavedObjectsRepository', () => {
},
};

const workspaces = ['workspace1', 'workspace2'];

const getMockBulkUpdateResponse = (objects, options, includeOriginId) => ({
items: objects.map(({ type, id }) => ({
update: {
Expand Down Expand Up @@ -1583,6 +1585,20 @@ describe('SavedObjectsRepository', () => {
});
});

it(`accepts workspaces property when providing workspaces info`, async () => {
const objects = [obj1, obj2].map((obj) => ({ ...obj, workspaces }));
await bulkUpdateSuccess(objects);
const doc = {
doc: expect.objectContaining({ workspaces }),
};
const body = [expect.any(Object), doc, expect.any(Object), doc];
expect(client.bulk).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
);
client.bulk.mockClear();
});

describe('errors', () => {
const obj = {
type: 'dashboard',
Expand Down Expand Up @@ -3950,6 +3966,8 @@ describe('SavedObjectsRepository', () => {
},
};

const workspaces = ['workspace1', 'workspace2'];

const updateSuccess = async (type, id, attributes, options, includeOriginId) => {
if (registry.isMultiNamespace(type)) {
const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace);
Expand Down Expand Up @@ -4137,6 +4155,18 @@ describe('SavedObjectsRepository', () => {
expect.anything()
);
});

it(`accepts workspaces when providing permissions info`, async () => {
await updateSuccess(type, id, attributes, { workspaces });
const expected = expect.objectContaining({ workspaces });
const body = {
doc: expected,
};
expect(client.update).toHaveBeenCalledWith(
expect.objectContaining({ body }),
expect.anything()
);
});
});

describe('errors', () => {
Expand Down
32 changes: 27 additions & 5 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,13 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}

const { version, references, refresh = DEFAULT_REFRESH_SETTING, permissions } = options;
const {
version,
references,
refresh = DEFAULT_REFRESH_SETTING,
permissions,
workspaces,
} = options;
const namespace = normalizeNamespace(options.namespace);

let preflightResult: SavedObjectsRawDoc | undefined;
Expand All @@ -1104,6 +1110,7 @@ export class SavedObjectsRepository {
updated_at: time,
...(Array.isArray(references) && { references }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
};

const { body, statusCode } = await this.client.update<SavedObjectsRawDocSource>(
Expand Down Expand Up @@ -1142,6 +1149,7 @@ export class SavedObjectsRepository {
namespaces,
...(originId && { originId }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
references,
attributes,
};
Expand Down Expand Up @@ -1342,7 +1350,14 @@ export class SavedObjectsRepository {
};
}

const { attributes, references, version, namespace: objectNamespace, permissions } = object;
const {
attributes,
references,
version,
namespace: objectNamespace,
permissions,
workspaces,
} = object;

if (objectNamespace === ALL_NAMESPACES_STRING) {
return {
Expand All @@ -1364,6 +1379,7 @@ export class SavedObjectsRepository {
updated_at: time,
...(Array.isArray(references) && { references }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
};

const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type);
Expand Down Expand Up @@ -1515,8 +1531,13 @@ export class SavedObjectsRepository {
response
)[0] as any;

// eslint-disable-next-line @typescript-eslint/naming-convention
const { [type]: attributes, references, updated_at, permissions } = documentToSave;
const {
[type]: attributes,
references,
updated_at: updatedAt,
permissions,
workspaces,
} = documentToSave;
if (error) {
return {
id,
Expand All @@ -1531,11 +1552,12 @@ export class SavedObjectsRepository {
type,
...(namespaces && { namespaces }),
...(originId && { originId }),
updated_at,
updated_at: updatedAt,
version: encodeVersion(seqNo, primaryTerm),
attributes,
references,
...(permissions && { permissions }),
...(workspaces && { workspaces }),
};
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const create = () =>
update: jest.fn(),
addToNamespaces: jest.fn(),
deleteFromNamespaces: jest.fn(),
addToWorkspaces: jest.fn(),
deleteFromWorkspaces: jest.fn(),
} as unknown) as jest.Mocked<SavedObjectsClientContract>);

export const savedObjectsClientMock = { create };
84 changes: 84 additions & 0 deletions src/core/server/saved_objects/service/saved_objects_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,87 @@ test(`#deleteByWorkspace`, async () => {
expect(mockRepository.deleteByWorkspace).toHaveBeenCalledWith(workspace, options);
expect(result).toBe(returnValue);
});

test(`#deleteFromWorkspaces Should use update if there is existing workspaces`, async () => {
const returnValue = Symbol();
const create = jest.fn();
const mockRepository = {
get: jest.fn().mockResolvedValue({
workspaces: ['id1', 'id2'],
}),
update: jest.fn().mockResolvedValue(returnValue),
create,
};
const client = new SavedObjectsClient(mockRepository);

const type = Symbol();
const id = Symbol();
await client.deleteFromWorkspaces(type, id, ['id2']);
expect(mockRepository.get).toHaveBeenCalledWith(type, id, {});
expect(mockRepository.update).toHaveBeenCalledWith(type, id, undefined, {
version: undefined,
workspaces: ['id1'],
});
});

test(`#deleteFromWorkspaces Should use overwrite create if there is no existing workspaces`, async () => {
const returnValue = Symbol();
const create = jest.fn();
const mockRepository = {
get: jest.fn().mockResolvedValue({
workspaces: [],
}),
update: jest.fn().mockResolvedValue(returnValue),
create,
};
const client = new SavedObjectsClient(mockRepository);

const type = Symbol();
const id = Symbol();
await client.deleteFromWorkspaces(type, id, ['id1']);
expect(mockRepository.get).toHaveBeenCalledWith(type, id, {});
expect(mockRepository.create).toHaveBeenCalledWith(
type,
{},
{ id, overwrite: true, permissions: undefined, version: undefined }
);
});

test(`#deleteFromWorkspaces should throw error if no workspaces passed`, () => {
const mockRepository = {};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const workspaces = [];
expect(() => client.deleteFromWorkspaces(type, id, workspaces)).rejects.toThrowError();
});

test(`#addToWorkspaces`, async () => {
const returnValue = Symbol();
const mockRepository = {
get: jest.fn().mockResolvedValue(returnValue),
update: jest.fn().mockResolvedValue(returnValue),
};
const client = new SavedObjectsClient(mockRepository);

const type = Symbol();
const id = Symbol();
const workspaces = Symbol();
const result = await client.addToWorkspaces(type, id, workspaces);

expect(mockRepository.get).toHaveBeenCalledWith(type, id, {});
expect(mockRepository.update).toHaveBeenCalledWith(type, id, undefined, {
workspaces: [workspaces],
});

expect(result).toBe(returnValue);
});

test(`#addToWorkspaces should throw error if no workspaces passed`, () => {
const mockRepository = {};
const client = new SavedObjectsClient(mockRepository);
const type = Symbol();
const id = Symbol();
const workspaces = [];
expect(() => client.addToWorkspaces(type, id, workspaces)).rejects.toThrowError();
});
65 changes: 64 additions & 1 deletion src/core/server/saved_objects/service/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export interface SavedObjectsBulkCreateObject<T = unknown> {
* @public
*/
export interface SavedObjectsBulkUpdateObject<T = unknown>
extends Pick<SavedObjectsUpdateOptions, 'version' | 'references' | 'permissions'> {
extends Pick<SavedObjectsUpdateOptions, 'version' | 'references' | 'permissions' | 'workspaces'> {
/** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */
id: string;
/** The type of this Saved Object. Each plugin can define it's own custom Saved Object types. */
Expand Down Expand Up @@ -189,6 +189,7 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
refresh?: MutatingOperationRefreshSetting;
/** permission control describe by ACL object */
permissions?: Permissions;
workspaces?: string[];
}

/**
Expand Down Expand Up @@ -463,6 +464,68 @@ export class SavedObjectsClient {
return await this._repository.deleteByWorkspace(workspace, options);
};

/**
* Remove a saved object from workspaces
* @param type
* @param id
* @param workspaces
*/
deleteFromWorkspaces = async <T = unknown>(type: string, id: string, workspaces: string[]) => {
if (!workspaces || workspaces.length === 0) {
throw new TypeError(`Workspaces is required.`);
}
const object = await this.get<T>(type, id);
const existingWorkspaces = object.workspaces ?? [];
const newWorkspaces = existingWorkspaces.filter((item) => {
return workspaces.indexOf(item) === -1;
});
if (newWorkspaces.length > 0) {
return await this.update<T>(type, id, object.attributes, {
workspaces: newWorkspaces,
version: object.version,
});
} else {
// If there is no workspaces assigned, will create object with overwrite to delete workspace property.
return await this.create(
type,
{
...object.attributes,
},
{
id,
permissions: object.permissions,
overwrite: true,
version: object.version,
}
);
}
};

/**
* Add a saved object to workspaces
* @param type
* @param id
* @param workspaces
*/
addToWorkspaces = async <T = unknown>(
type: string,
id: string,
workspaces: string[]
): Promise<any> => {
if (!workspaces || workspaces.length === 0) {
throw new TypeError(`Workspaces is required.`);
}
const object = await this.get<T>(type, id);
const existingWorkspaces = object.workspaces ?? [];
const mergedWorkspaces = existingWorkspaces.concat(workspaces);
const nonDuplicatedWorkspaces = Array.from(new Set(mergedWorkspaces));

return await this.update<T>(type, id, object.attributes, {
workspaces: nonDuplicatedWorkspaces,
version: object.version,
});
};

/**
* Bulk Updates multiple SavedObject at once
*
Expand Down
11 changes: 11 additions & 0 deletions src/plugins/workspace/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { DataSourceAttributes } from 'src/plugins/data_source/common/data_sources';

export type DataSource = Pick<DataSourceAttributes, 'title'> & {
// Id defined in SavedObjectAttribute could be single or array, here only should be single string.
id: string;
};
Loading

0 comments on commit 5d33328

Please sign in to comment.