From ee8e94aa1e317e65c84d57071e5f533dead8a597 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 12 Mar 2024 10:28:42 +0800 Subject: [PATCH 1/5] Add workspacePluginSetup type Signed-off-by: Hailong Cui --- src/plugins/workspace/server/index.ts | 2 +- src/plugins/workspace/server/plugin.ts | 4 ++-- src/plugins/workspace/server/types.ts | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugins/workspace/server/index.ts b/src/plugins/workspace/server/index.ts index fe44b4d71757..3a8e9ca50fcc 100644 --- a/src/plugins/workspace/server/index.ts +++ b/src/plugins/workspace/server/index.ts @@ -18,4 +18,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export { WorkspaceFindOptions } from './types'; +export { WorkspaceFindOptions, WorkspacePluginSetup } from './types'; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index e4ed75bad615..bea139facb12 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -10,13 +10,13 @@ import { Logger, CoreStart, } from '../../../core/server'; -import { IWorkspaceClientImpl } from './types'; +import { IWorkspaceClientImpl, WorkspacePluginSetup } from './types'; import { WorkspaceClient } from './workspace_client'; import { registerRoutes } from './routes'; import { WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper_for_check_workspace_conflict'; -export class WorkspacePlugin implements Plugin<{}, {}> { +export class WorkspacePlugin implements Plugin { private readonly logger: Logger; private client?: IWorkspaceClientImpl; private workspaceConflictControl?: WorkspaceConflictSavedObjectsClientWrapper; diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index 0f60597a7a8a..92848279f360 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -117,3 +117,7 @@ export type IResponse = success: false; error?: string; }; + +export interface WorkspacePluginSetup { + client: IWorkspaceClientImpl; +} From edf435e9dbac223f851a5a35ea32856ed40fa4e8 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 12 Mar 2024 15:42:29 +0800 Subject: [PATCH 2/5] add fakenews.co to .lycheeexclude Signed-off-by: Hailong Cui --- .lycheeexclude | 1 + 1 file changed, 1 insertion(+) diff --git a/.lycheeexclude b/.lycheeexclude index 67ed88344a25..636c832d1709 100644 --- a/.lycheeexclude +++ b/.lycheeexclude @@ -90,6 +90,7 @@ https://www.hostedgraphite.com/ https://connectionurl.com http://169.254.169.254/latest/meta-data/ http://company.net/* +http://fakenews.co/opensearch-dashboards-test/v6.8.2.json # External urls https://www.zeek.org/ From 98dadcf977eceb41b76e788bf94341174c01ad4e Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Tue, 12 Mar 2024 14:07:09 +0800 Subject: [PATCH 3/5] [Workspace] Add a workspace client in workspace plugin (#6094) * feat: add comment Signed-off-by: SuZhou-Joe * feat: update unit test Signed-off-by: SuZhou-Joe * feat: add CHANGELOG Signed-off-by: SuZhou-Joe * feat: optimize comment Signed-off-by: SuZhou-Joe * feat: optimize comment Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe * feat: optimize code Signed-off-by: SuZhou-Joe --------- Signed-off-by: SuZhou-Joe Signed-off-by: Hailong Cui --- CHANGELOG.md | 2 +- src/plugins/workspace/public/plugin.test.ts | 18 +- src/plugins/workspace/public/plugin.ts | 7 +- .../workspace/public/workspace_client.mock.ts | 25 ++ .../workspace/public/workspace_client.test.ts | 212 +++++++++++++ .../workspace/public/workspace_client.ts | 294 ++++++++++++++++++ 6 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 src/plugins/workspace/public/workspace_client.mock.ts create mode 100644 src/plugins/workspace/public/workspace_client.test.ts create mode 100644 src/plugins/workspace/public/workspace_client.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bba980268a..2f5d5ee061f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Multiple Datasource] Test connection schema validation for registered auth types ([#6109](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6109)) - [Workspace] Consume workspace id in saved object client ([#6014](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6014)) - [Multiple Datasource] Export DataSourcePluginRequestContext at top level for plugins to use ([#6108](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6108)) - - [Workspace] Add delete saved objects by workspace functionality([#6013](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6013)) +- [Workspace] Add a workspace client in workspace plugin ([#6094](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6094)) ### 🐛 Bug Fixes diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index e1a45ee115ab..8c869415aede 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -3,10 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { coreMock } from '../../../core/public/mocks'; +import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.mock'; +import { chromeServiceMock, coreMock } from '../../../core/public/mocks'; import { WorkspacePlugin } from './plugin'; describe('Workspace plugin', () => { + const getSetupMock = () => ({ + ...coreMock.createSetup(), + chrome: chromeServiceMock.createSetupContract(), + }); + beforeEach(() => { + WorkspaceClientMock.mockClear(); + Object.values(workspaceClientMock).forEach((item) => item.mockClear()); + }); + it('#setup', async () => { + const setupMock = getSetupMock(); + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock); + expect(WorkspaceClientMock).toBeCalledTimes(1); + }); + it('#call savedObjectsClient.setCurrentWorkspace when current workspace id changed', () => { const workspacePlugin = new WorkspacePlugin(); const coreStart = coreMock.createStart(); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 3840066fcee3..6f604bcf5678 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -4,7 +4,8 @@ */ import type { Subscription } from 'rxjs'; -import { Plugin, CoreStart } from '../../../core/public'; +import { Plugin, CoreStart, CoreSetup } from '../../../core/public'; +import { WorkspaceClient } from './workspace_client'; export class WorkspacePlugin implements Plugin<{}, {}, {}> { private coreStart?: CoreStart; @@ -18,7 +19,9 @@ export class WorkspacePlugin implements Plugin<{}, {}, {}> { }); } } - public async setup() { + public async setup(core: CoreSetup) { + const workspaceClient = new WorkspaceClient(core.http, core.workspaces); + await workspaceClient.init(); return {}; } diff --git a/src/plugins/workspace/public/workspace_client.mock.ts b/src/plugins/workspace/public/workspace_client.mock.ts new file mode 100644 index 000000000000..2ceeae5627d1 --- /dev/null +++ b/src/plugins/workspace/public/workspace_client.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const workspaceClientMock = { + init: jest.fn(), + enterWorkspace: jest.fn(), + getCurrentWorkspaceId: jest.fn(), + getCurrentWorkspace: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + list: jest.fn(), + get: jest.fn(), + update: jest.fn(), + stop: jest.fn(), +}; + +export const WorkspaceClientMock = jest.fn(function () { + return workspaceClientMock; +}); + +jest.doMock('./workspace_client', () => ({ + WorkspaceClient: WorkspaceClientMock, +})); diff --git a/src/plugins/workspace/public/workspace_client.test.ts b/src/plugins/workspace/public/workspace_client.test.ts new file mode 100644 index 000000000000..c18ed3db64e7 --- /dev/null +++ b/src/plugins/workspace/public/workspace_client.test.ts @@ -0,0 +1,212 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { httpServiceMock, workspacesServiceMock } from '../../../core/public/mocks'; +import { WorkspaceClient } from './workspace_client'; + +const getWorkspaceClient = () => { + const httpSetupMock = httpServiceMock.createSetupContract(); + const workspaceMock = workspacesServiceMock.createSetupContract(); + return { + httpSetupMock, + workspaceMock, + workspaceClient: new WorkspaceClient(httpSetupMock, workspaceMock), + }; +}; + +describe('#WorkspaceClient', () => { + it('#init', async () => { + const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient(); + await workspaceClient.init(); + expect(workspaceMock.initialized$.getValue()).toEqual(true); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', { + method: 'POST', + body: JSON.stringify({ + perPage: 999, + }), + }); + }); + + it('#enterWorkspace', async () => { + const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: false, + }); + const result = await workspaceClient.enterWorkspace('foo'); + expect(result.success).toEqual(false); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + }); + const successResult = await workspaceClient.enterWorkspace('foo'); + expect(workspaceMock.currentWorkspaceId$.getValue()).toEqual('foo'); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', { + method: 'GET', + }); + expect(successResult.success).toEqual(true); + }); + + it('#getCurrentWorkspaceId', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + }); + await workspaceClient.enterWorkspace('foo'); + expect(workspaceClient.getCurrentWorkspaceId()).toEqual({ + success: true, + result: 'foo', + }); + }); + + it('#getCurrentWorkspace', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + result: { + name: 'foo', + }, + }); + await workspaceClient.enterWorkspace('foo'); + expect(await workspaceClient.getCurrentWorkspace()).toEqual({ + success: true, + result: { + name: 'foo', + }, + }); + }); + + it('#create', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + result: { + name: 'foo', + workspaces: [], + }, + }); + await workspaceClient.create({ + name: 'foo', + }); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces', { + method: 'POST', + body: JSON.stringify({ + attributes: { + name: 'foo', + }, + }), + }); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', { + method: 'POST', + body: JSON.stringify({ + perPage: 999, + }), + }); + }); + + it('#delete', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + result: { + name: 'foo', + workspaces: [], + }, + }); + await workspaceClient.delete('foo'); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', { + method: 'DELETE', + }); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', { + method: 'POST', + body: JSON.stringify({ + perPage: 999, + }), + }); + }); + + it('#list', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + result: { + workspaces: [], + }, + }); + await workspaceClient.list({ + perPage: 999, + }); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', { + method: 'POST', + body: JSON.stringify({ + perPage: 999, + }), + }); + }); + + it('#get', async () => { + const { workspaceClient, httpSetupMock } = getWorkspaceClient(); + await workspaceClient.get('foo'); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', { + method: 'GET', + }); + }); + + it('#update', async () => { + const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient(); + httpSetupMock.fetch.mockResolvedValue({ + success: true, + result: { + workspaces: [ + { + id: 'foo', + }, + ], + }, + }); + await workspaceClient.update('foo', { + name: 'foo', + }); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/foo', { + method: 'PUT', + body: JSON.stringify({ + attributes: { + name: 'foo', + }, + }), + }); + expect(workspaceMock.workspaceList$.getValue()).toEqual([ + { + id: 'foo', + }, + ]); + expect(httpSetupMock.fetch).toBeCalledWith('/api/workspaces/_list', { + method: 'POST', + body: JSON.stringify({ + perPage: 999, + }), + }); + }); + + it('#update with list gives error', async () => { + const { workspaceClient, httpSetupMock, workspaceMock } = getWorkspaceClient(); + let callTimes = 0; + httpSetupMock.fetch.mockImplementation(async () => { + callTimes++; + if (callTimes > 1) { + return { + success: false, + error: 'Something went wrong', + }; + } + + return { + success: true, + }; + }); + await workspaceClient.update('foo', { + name: 'foo', + }); + expect(workspaceMock.workspaceList$.getValue()).toEqual([]); + }); +}); diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts new file mode 100644 index 000000000000..3e988f38b265 --- /dev/null +++ b/src/plugins/workspace/public/workspace_client.ts @@ -0,0 +1,294 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + HttpFetchError, + HttpFetchOptions, + HttpSetup, + WorkspaceAttribute, + WorkspacesSetup, +} from '../../../core/public'; + +const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +const join = (...uriComponents: Array) => + uriComponents + .filter((comp): comp is string => Boolean(comp)) + .map(encodeURIComponent) + .join('/'); + +type IResponse = + | { + result: T; + success: true; + } + | { + success: false; + error?: string; + }; + +interface WorkspaceFindOptions { + page?: number; + perPage?: number; + search?: string; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; +} + +/** + * Workspaces is OpenSearchDashboards's visualize mechanism allowing admins to + * organize related features + * + * @public + */ +export class WorkspaceClient { + private http: HttpSetup; + private workspaces: WorkspacesSetup; + + constructor(http: HttpSetup, workspaces: WorkspacesSetup) { + this.http = http; + this.workspaces = workspaces; + } + + /** + * Initialize workspace list: + * 1. Retrieve the list of workspaces + * 2. Change the initialized flag to true + */ + public async init() { + await this.updateWorkspaceList(); + this.workspaces.initialized$.next(true); + } + + /** + * Add a non-throw-error fetch method, + * so that consumers only need to care about + * if the success is false instead of wrapping the call with a try catch + * and judge the error both in catch clause and if(!success) cluase. + */ + private safeFetch = async ( + path: string, + options: HttpFetchOptions + ): Promise> => { + try { + return await this.http.fetch>(path, options); + } catch (error: unknown) { + if (error instanceof HttpFetchError) { + return { + success: false, + error: error.body?.message || error.body?.error || error.message, + }; + } + + if (error instanceof Error) { + return { + success: false, + error: error.message, + }; + } + + return { + success: false, + error: 'Unknown error', + }; + } + }; + + /** + * Filter empty sub path and join all of the sub paths into a standard http path + * + * @param path + * @returns path + */ + private getPath(...path: Array): string { + return [WORKSPACES_API_BASE_URL, join(...path)].filter((item) => item).join('/'); + } + + /** + * Fetch latest list of workspaces and update workspaceList$ to notify subscriptions + */ + private async updateWorkspaceList(): Promise { + const result = await this.list({ + perPage: 999, + }); + + if (result?.success) { + this.workspaces.workspaceList$.next(result.result.workspaces); + } else { + this.workspaces.workspaceList$.next([]); + } + } + + /** + * This method will check if a valid workspace can be found by the given workspace id, + * If so, perform a side effect of updating the core.workspace.currentWorkspaceId$. + * + * @param id workspace id + * @returns {Promise>} result for this operation + */ + public async enterWorkspace(id: string): Promise> { + const workspaceResp = await this.get(id); + if (workspaceResp.success) { + this.workspaces.currentWorkspaceId$.next(id); + return { + success: true, + result: null, + }; + } else { + return workspaceResp; + } + } + + /** + * A bypass layer to get current workspace id + */ + public getCurrentWorkspaceId(): IResponse { + const currentWorkspaceId = this.workspaces.currentWorkspaceId$.getValue(); + if (!currentWorkspaceId) { + return { + success: false, + error: i18n.translate('workspace.error.notInWorkspace', { + defaultMessage: 'You are not in any workspace yet.', + }), + }; + } + + return { + success: true, + result: currentWorkspaceId, + }; + } + + /** + * Do a find in the latest workspace list with current workspace id + */ + public async getCurrentWorkspace(): Promise> { + const currentWorkspaceIdResp = this.getCurrentWorkspaceId(); + if (currentWorkspaceIdResp.success) { + const currentWorkspaceResp = await this.get(currentWorkspaceIdResp.result); + return currentWorkspaceResp; + } else { + return currentWorkspaceIdResp; + } + } + + /** + * Create a workspace + * + * @param attributes + * @returns {Promise>>} id of the new created workspace + */ + public async create( + attributes: Omit + ): Promise>> { + const path = this.getPath(); + + const result = await this.safeFetch(path, { + method: 'POST', + body: JSON.stringify({ + attributes, + }), + }); + + if (result.success) { + await this.updateWorkspaceList(); + } + + return result; + } + + /** + * Deletes a workspace by workspace id + * + * @param id + * @returns {Promise>} result for this operation + */ + public async delete(id: string): Promise> { + const result = await this.safeFetch(this.getPath(id), { method: 'DELETE' }); + + if (result.success) { + await this.updateWorkspaceList(); + } + + return result; + } + + /** + * Search for workspaces + * + * @param {object} [options={}] + * @property {string} options.search + * @property {string} options.searchFields - see OpenSearch Simple Query String + * Query field argument for more information + * @property {integer} [options.page=1] + * @property {integer} [options.perPage=20] + * @property {array} options.fields + * @returns A find result with workspaces matching the specified search. + */ + public list( + options?: WorkspaceFindOptions + ): Promise< + IResponse<{ + workspaces: WorkspaceAttribute[]; + total: number; + per_page: number; + page: number; + }> + > { + const path = this.getPath('_list'); + return this.safeFetch(path, { + method: 'POST', + body: JSON.stringify(options || {}), + }); + } + + /** + * Fetches a single workspace by a workspace id + * + * @param {string} id + * @returns {Promise>} The metadata of the workspace for the given id. + */ + public get(id: string): Promise> { + const path = this.getPath(id); + return this.safeFetch(path, { + method: 'GET', + }); + } + + /** + * Updates a workspace + * + * @param {string} id + * @param {object} attributes + * @returns {Promise>} result for this operation + */ + public async update( + id: string, + attributes: Partial + ): Promise> { + const path = this.getPath(id); + const body = { + attributes, + }; + + const result = await this.safeFetch(path, { + method: 'PUT', + body: JSON.stringify(body), + }); + + if (result.success) { + await this.updateWorkspaceList(); + } + + return result; + } + + public stop() { + this.workspaces.workspaceList$.unsubscribe(); + this.workspaces.currentWorkspaceId$.unsubscribe(); + } +} From 7e0b2a4e22ee5b3fcbf5340f5b990ff6aae4eda6 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 12 Mar 2024 16:02:35 +0800 Subject: [PATCH 4/5] Revert "add fakenews.co to .lycheeexclude" This reverts commit 55f63448c58ac62287cb09da1105a7b4a0b3e24e. Signed-off-by: Hailong Cui --- .lycheeexclude | 1 - 1 file changed, 1 deletion(-) diff --git a/.lycheeexclude b/.lycheeexclude index 636c832d1709..67ed88344a25 100644 --- a/.lycheeexclude +++ b/.lycheeexclude @@ -90,7 +90,6 @@ https://www.hostedgraphite.com/ https://connectionurl.com http://169.254.169.254/latest/meta-data/ http://company.net/* -http://fakenews.co/opensearch-dashboards-test/v6.8.2.json # External urls https://www.zeek.org/ From e9a969a7a57ef2fcfa4511b74fda22afc508e097 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 12 Mar 2024 16:24:08 +0800 Subject: [PATCH 5/5] Add WorkspacePluginStart Signed-off-by: Hailong Cui --- src/plugins/workspace/server/index.ts | 2 +- src/plugins/workspace/server/plugin.ts | 4 ++-- src/plugins/workspace/server/types.ts | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugins/workspace/server/index.ts b/src/plugins/workspace/server/index.ts index 3a8e9ca50fcc..2cfd05aaea8a 100644 --- a/src/plugins/workspace/server/index.ts +++ b/src/plugins/workspace/server/index.ts @@ -18,4 +18,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export { WorkspaceFindOptions, WorkspacePluginSetup } from './types'; +export { WorkspaceFindOptions, WorkspacePluginSetup, WorkspacePluginStart } from './types'; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index bea139facb12..5ad4df46c919 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -10,13 +10,13 @@ import { Logger, CoreStart, } from '../../../core/server'; -import { IWorkspaceClientImpl, WorkspacePluginSetup } from './types'; +import { IWorkspaceClientImpl, WorkspacePluginSetup, WorkspacePluginStart } from './types'; import { WorkspaceClient } from './workspace_client'; import { registerRoutes } from './routes'; import { WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; import { WorkspaceConflictSavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper_for_check_workspace_conflict'; -export class WorkspacePlugin implements Plugin { +export class WorkspacePlugin implements Plugin { private readonly logger: Logger; private client?: IWorkspaceClientImpl; private workspaceConflictControl?: WorkspaceConflictSavedObjectsClientWrapper; diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index 92848279f360..29e8747c7618 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -121,3 +121,7 @@ export type IResponse = export interface WorkspacePluginSetup { client: IWorkspaceClientImpl; } + +export interface WorkspacePluginStart { + client: IWorkspaceClientImpl; +}