diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c8227d9765..8abc72aeb779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Adds Data explorer framework and implements Discover using it ([#4806](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4806)) - [Theme] Use themes' definitions to render the initial view ([#4936](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4936/)) - [Theme] Make `next` theme the default ([#4854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4854/)) +- [Workspace] Setup workspace skeleton and implement basic CRUD API ([#5075](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5075/)) ### 🐛 Bug Fixes diff --git a/src/plugins/workspace/server/integration_tests/routes.test.ts b/src/plugins/workspace/server/integration_tests/routes.test.ts new file mode 100644 index 000000000000..a83c908b7d10 --- /dev/null +++ b/src/plugins/workspace/server/integration_tests/routes.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WorkspaceAttribute } from 'src/core/types'; +import { omit } from 'lodash'; +import * as osdTestServer from '../../../../core/test_helpers/osd_server'; +import { WorkspaceRoutePermissionItem } from '../types'; +import { WorkspacePermissionMode } from '../../../../core/server'; + +const testWorkspace: WorkspaceAttribute & { + permissions: WorkspaceRoutePermissionItem; +} = { + id: 'fake_id', + name: 'test_workspace', + description: 'test_workspace_description', +}; + +describe('workspace service', () => { + let root: ReturnType; + let opensearchServer: osdTestServer.TestOpenSearchUtils; + beforeAll(async () => { + const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + osd: { + workspace: { + enabled: true, + }, + }, + }, + }); + opensearchServer = await startOpenSearch(); + const startOSDResp = await startOpenSearchDashboards(); + root = startOSDResp.root; + }, 30000); + afterAll(async () => { + await root.shutdown(); + await opensearchServer.stop(); + }); + describe('Workspace CRUD apis', () => { + afterEach(async () => { + const listResult = await osdTestServer.request + .post(root, `/api/workspaces/_list`) + .send({ + page: 1, + }) + .expect(200); + await Promise.all( + listResult.body.result.workspaces.map((item: WorkspaceAttribute) => + osdTestServer.request.delete(root, `/api/workspaces/${item.id}`).expect(200) + ) + ); + }); + it('create', async () => { + await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: testWorkspace, + }) + .expect(400); + + const result: any = await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .expect(200); + + expect(result.body.success).toEqual(true); + expect(typeof result.body.result.id).toBe('string'); + }); + it('get', async () => { + const result = await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .expect(200); + + const getResult = await osdTestServer.request.get( + root, + `/api/workspaces/${result.body.result.id}` + ); + expect(getResult.body.result.name).toEqual(testWorkspace.name); + }); + it('update', async () => { + const result: any = await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .expect(200); + + await osdTestServer.request + .put(root, `/api/workspaces/${result.body.result.id}`) + .send({ + attributes: { + ...omit(testWorkspace, 'id'), + name: 'updated', + }, + }) + .expect(200); + + const getResult = await osdTestServer.request.get( + root, + `/api/workspaces/${result.body.result.id}` + ); + + expect(getResult.body.success).toEqual(true); + expect(getResult.body.result.name).toEqual('updated'); + }); + it('delete', async () => { + const result: any = await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .expect(200); + + await osdTestServer.request + .delete(root, `/api/workspaces/${result.body.result.id}`) + .expect(200); + + const getResult = await osdTestServer.request.get( + root, + `/api/workspaces/${result.body.result.id}` + ); + + expect(getResult.body.success).toEqual(false); + }); + it('list', async () => { + await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .expect(200); + + const listResult = await osdTestServer.request + .post(root, `/api/workspaces/_list`) + .send({ + page: 1, + }) + .expect(200); + expect(listResult.body.result.total).toEqual(3); + }); + }); +}); diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts index 21475ede368b..00ad05f3c961 100644 --- a/src/plugins/workspace/server/routes/index.ts +++ b/src/plugins/workspace/server/routes/index.ts @@ -46,7 +46,9 @@ const workspaceAttributesSchema = schema.object({ icon: schema.maybe(schema.string()), reserved: schema.maybe(schema.boolean()), defaultVISTheme: schema.maybe(schema.string()), - permissions: schema.oneOf([workspacePermission, schema.arrayOf(workspacePermission)]), + permissions: schema.maybe( + schema.oneOf([workspacePermission, schema.arrayOf(workspacePermission)]) + ), }); const convertToACL = ( @@ -126,7 +128,9 @@ export function registerRoutes({ ...result.result, workspaces: result.result.workspaces.map((workspace) => ({ ...workspace, - permissions: convertFromACL(workspace.permissions), + ...(workspace.permissions + ? { permissions: convertFromACL(workspace.permissions) } + : {}), })), }, }, @@ -161,7 +165,9 @@ export function registerRoutes({ ...result, result: { ...result.result, - permissions: convertFromACL(result.result.permissions), + ...(result.result.permissions + ? { permissions: convertFromACL(result.result.permissions) } + : {}), }, }, }); @@ -180,9 +186,13 @@ export function registerRoutes({ const { attributes } = req.body; const rawRequest = ensureRawRequest(req); const authInfo = rawRequest?.auth?.credentials?.authInfo as { user_name?: string } | null; - const permissions = Array.isArray(attributes.permissions) - ? attributes.permissions - : [attributes.permissions]; + const { permissions: permissionsInAttributes, ...others } = attributes; + let permissions: WorkspaceRoutePermissionItem[] = []; + if (permissionsInAttributes) { + permissions = Array.isArray(permissionsInAttributes) + ? permissionsInAttributes + : [permissionsInAttributes]; + } if (!!authInfo?.user_name) { permissions.push({ @@ -204,8 +214,8 @@ export function registerRoutes({ logger, }, { - ...attributes, - permissions: convertToACL(permissions), + ...others, + ...(permissions.length ? { permissions: convertToACL(permissions) } : {}), } ); return res.ok({ body: result }); @@ -226,6 +236,11 @@ export function registerRoutes({ router.handleLegacyErrors(async (context, req, res) => { const { id } = req.params; const { attributes } = req.body; + const { permissions, ...others } = attributes; + let finalPermissions: WorkspaceRoutePermissionItem[] = []; + if (permissions) { + finalPermissions = Array.isArray(permissions) ? permissions : [permissions]; + } const result = await client.update( { @@ -235,8 +250,8 @@ export function registerRoutes({ }, id, { - ...attributes, - permissions: convertToACL(attributes.permissions), + ...others, + ...(finalPermissions.length ? { permissions: convertToACL(finalPermissions) } : {}), } ); return res.ok({ body: result }); diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index a0e6c7c2025e..2f46f5c7eb16 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -15,7 +15,7 @@ import { } from '../../../core/server'; export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { - permissions: Permissions; + permissions?: Permissions; } export interface WorkspaceFindOptions { diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 54ffe6e774a5..2d870d88251d 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -45,5 +45,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); loadTestFile(require.resolve('./telemetry')); + loadTestFile(require.resolve('./workspace')); }); } diff --git a/test/api_integration/apis/workspace/index.ts b/test/api_integration/apis/workspace/index.ts new file mode 100644 index 000000000000..553ee0dce1af --- /dev/null +++ b/test/api_integration/apis/workspace/index.ts @@ -0,0 +1,132 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { WorkspaceAttribute } from 'opensearch-dashboards/server'; +import { omit } from 'lodash'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const testWorkspace: WorkspaceAttribute = { + id: 'fake_id', + name: 'test_workspace', + description: 'test_workspace_description', +}; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Workspace CRUD apis', () => { + afterEach(async () => { + const listResult = await supertest + .post(`/api/workspaces/_list`) + .send({ + page: 1, + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + await Promise.all( + listResult.body.result.workspaces.map((item: WorkspaceAttribute) => + supertest + .delete(`/api/workspaces/${item.id}`) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200) + ) + ); + }); + it('create', async () => { + await supertest + .post(`/api/workspaces`) + .send({ + attributes: testWorkspace, + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(400); + + const result: any = await supertest + .post(`/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + expect(result.body.success).equal(true); + expect(result.body.result.id).to.be.a('string'); + }); + it('get', async () => { + const result = await supertest + .post(`/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + const getResult = await supertest.get(`/api/workspaces/${result.body.result.id}`); + expect(getResult.body.result.name).equal(testWorkspace.name); + }); + it('update', async () => { + const result: any = await supertest + .post(`/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + await supertest + .put(`/api/workspaces/${result.body.result.id}`) + .send({ + attributes: { + ...omit(testWorkspace, 'id'), + name: 'updated', + }, + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + const getResult = await supertest.get(`/api/workspaces/${result.body.result.id}`); + + expect(getResult.body.success).equal(true); + expect(getResult.body.result.name).equal('updated'); + }); + it('delete', async () => { + const result: any = await supertest + .post(`/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + await supertest + .delete(`/api/workspaces/${result.body.result.id}`) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + const getResult = await supertest.get(`/api/workspaces/${result.body.result.id}`); + + expect(getResult.body.success).equal(false); + }); + it('list', async () => { + await supertest + .post(`/api/workspaces`) + .send({ + attributes: omit(testWorkspace, 'id'), + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + + const listResult = await supertest + .post(`/api/workspaces/_list`) + .send({ + page: 1, + }) + .set('osd-xsrf', 'opensearch-dashboards') + .expect(200); + expect(listResult.body.result.total).equal(1); + }); + }).tags('is:workspace'); +}