From 83779148ee86f78036dea42658c5a2eba67e1070 Mon Sep 17 00:00:00 2001 From: suzhou Date: Thu, 15 Jun 2023 10:28:09 +0800 Subject: [PATCH] Add validation when load page (#8) * fix: validation & query Signed-off-by: SuZhoue-Joe * feat: modify file name to reduce confusion Signed-off-by: SuZhoue-Joe * feat: add landing logic to retrive workspace id Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * feat: add worklist observable Signed-off-by: SuZhoue-Joe * fix: type error Signed-off-by: SuZhoue-Joe * fix: type error Signed-off-by: SuZhoue-Joe * feat: make client more robust Signed-off-by: SuZhoue-Joe * feat: use Subject Signed-off-by: SuZhoue-Joe --------- Signed-off-by: SuZhoue-Joe --- src/core/public/core_app/core_app.ts | 2 + src/core/public/core_system.ts | 6 +- .../fatal_errors/fatal_errors_service.mock.ts | 30 +++ src/core/public/index.ts | 5 +- src/core/public/plugins/plugin_context.ts | 1 + .../public/plugins/plugins_service.test.ts | 7 +- src/core/public/workspace/consts.ts | 8 + src/core/public/workspace/index.ts | 5 +- .../public/workspace/workspaces_client.ts | 173 +++++++++++++++--- .../public/workspace/workspaces_service.ts | 49 ++++- .../integration_tests/core_services.test.ts | 4 +- src/core/server/internal_types.ts | 3 - src/core/server/server.ts | 6 +- src/core/server/workspaces/index.ts | 2 +- src/core/server/workspaces/routes/index.ts | 96 ++-------- .../workspaces/saved_objects/workspace.ts | 4 +- src/core/server/workspaces/types.ts | 12 +- ...h_saved_object.ts => workspaces_client.ts} | 0 .../server/workspaces/workspaces_service.ts | 12 +- src/plugins/workspace/common/constants.ts | 1 + src/plugins/workspace/public/plugin.ts | 66 ++++++- 21 files changed, 340 insertions(+), 152 deletions(-) create mode 100644 src/core/public/workspace/consts.ts rename src/core/server/workspaces/{workspaces_client_with_saved_object.ts => workspaces_client.ts} (100%) diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index fcbcc5de5655..c4d359d58dc1 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -42,12 +42,14 @@ import type { IUiSettingsClient } from '../ui_settings'; import type { InjectedMetadataSetup } from '../injected_metadata'; import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; import { renderApp as renderStatusApp } from './status'; +import { WorkspacesSetup } from '../workspace'; interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; injectedMetadata: InjectedMetadataSetup; notifications: NotificationsSetup; + workspaces: WorkspacesSetup; } interface StartDeps { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 27f39ea57c1b..1c0286cbd03c 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -163,13 +163,14 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); + const workspaces = await this.workspaces.setup({ http }); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies: new Map([...pluginDependencies]), }); const application = this.application.setup({ context, http }); - this.coreApp.setup({ application, http, injectedMetadata, notifications }); + this.coreApp.setup({ application, http, injectedMetadata, notifications, workspaces }); const core: InternalCoreSetup = { application, @@ -179,6 +180,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }; // Services that do not expose contracts at setup @@ -202,7 +204,7 @@ export class CoreSystem { const uiSettings = await this.uiSettings.start(); const docLinks = this.docLinks.start({ injectedMetadata }); const http = await this.http.start(); - const workspaces = await this.workspaces.start({ http }); + const workspaces = await this.workspaces.start(); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const fatalErrors = await this.fatalErrors.start(); diff --git a/src/core/public/fatal_errors/fatal_errors_service.mock.ts b/src/core/public/fatal_errors/fatal_errors_service.mock.ts index ff1b252fc128..5079fc8f4b6a 100644 --- a/src/core/public/fatal_errors/fatal_errors_service.mock.ts +++ b/src/core/public/fatal_errors/fatal_errors_service.mock.ts @@ -30,6 +30,8 @@ import type { PublicMethodsOf } from '@osd/utility-types'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors_service'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { WorkspaceAttribute } from '../workspace'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { @@ -58,3 +60,31 @@ export const fatalErrorsServiceMock = { createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, }; + +const currentWorkspaceId$ = new BehaviorSubject(''); +const workspaceList$ = new Subject(); + +const createWorkspacesSetupContractMock = () => ({ + client: { + currentWorkspaceId$, + workspaceList$, + stop: jest.fn(), + enterWorkspace: jest.fn(), + exitWorkspace: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + list: jest.fn(), + getCurrentWorkspace: jest.fn(), + getCurrentWorkspaceId: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + formatUrlWithWorkspaceId: jest.fn(), +}); + +const createWorkspacesStartContractMock = createWorkspacesSetupContractMock; + +export const workspacesServiceMock = { + createSetupContractMock: createWorkspacesStartContractMock, + createStartContract: createWorkspacesStartContractMock, +}; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ab8a70b7c1fd..236048a012e7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -88,7 +88,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; -import { WorkspacesStart } from './workspace'; +import { WorkspacesStart, WorkspacesSetup } from './workspace'; export { PackageInfo, EnvironmentMode } from '../server/types'; /** @interal */ @@ -241,6 +241,8 @@ export interface CoreSetup; + /** {@link WorkspacesSetup} */ + workspaces: WorkspacesSetup; } /** @@ -354,4 +356,5 @@ export { WorkspacesService, WorkspaceAttribute, WorkspaceFindOptions, + WORKSPACE_ID_QUERYSTRING_NAME, } from './workspace'; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 81fcb2b34dab..87738fc7e57a 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -121,6 +121,7 @@ export function createPluginSetupContext< getBranding: deps.injectedMetadata.getBranding, }, getStartServices: () => plugin.startDependencies, + workspaces: deps.workspaces, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 5fa3bf888b5c..9c36a791332d 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -50,7 +50,10 @@ import { applicationServiceMock } from '../application/application_service.mock' import { i18nServiceMock } from '../i18n/i18n_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { + fatalErrorsServiceMock, + workspacesServiceMock, +} from '../fatal_errors/fatal_errors_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -108,6 +111,7 @@ describe('PluginsService', () => { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + workspaces: workspacesServiceMock.createSetupContractMock(), }; mockSetupContext = { ...mockSetupDeps, @@ -127,6 +131,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/workspace/consts.ts b/src/core/public/workspace/consts.ts new file mode 100644 index 000000000000..77f9144a27a3 --- /dev/null +++ b/src/core/public/workspace/consts.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index a70d91733ab6..d0fb17ead0c1 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -3,5 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ export { WorkspacesClientContract, WorkspacesClient } from './workspaces_client'; -export { WorkspacesStart, WorkspacesService } from './workspaces_service'; -export { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; +export { WorkspacesStart, WorkspacesService, WorkspacesSetup } from './workspaces_service'; +export type { WorkspaceAttribute, WorkspaceFindOptions } from '../../server/types'; +export { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; diff --git a/src/core/public/workspace/workspaces_client.ts b/src/core/public/workspace/workspaces_client.ts index 6b369517b366..8508921cbb0d 100644 --- a/src/core/public/workspace/workspaces_client.ts +++ b/src/core/public/workspace/workspaces_client.ts @@ -3,17 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ import { resolve as resolveUrl } from 'url'; -import type { PublicMethodsOf } from '@osd/utility-types'; -import { WORKSPACES_API_BASE_URL } from '../../server/types'; -import { HttpStart } from '../http'; +import type { PublicContract } from '@osd/utility-types'; +import { Subject } from 'rxjs'; +import { HttpFetchError, HttpFetchOptions, HttpSetup } from '../http'; import { WorkspaceAttribute, WorkspaceFindOptions } from '.'; +import { WORKSPACES_API_BASE_URL } from './consts'; /** * WorkspacesClientContract as implemented by the {@link WorkspacesClient} * * @public */ -export type WorkspacesClientContract = PublicMethodsOf; +export type WorkspacesClientContract = PublicContract; const join = (...uriComponents: Array) => uriComponents @@ -38,33 +39,107 @@ type IResponse = * @public */ export class WorkspacesClient { - private http: HttpStart; - constructor(http: HttpStart) { + private http: HttpSetup; + private currentWorkspaceId = ''; + public currentWorkspaceId$ = new Subject(); + public workspaceList$ = new Subject(); + constructor(http: HttpSetup) { this.http = http; + this.currentWorkspaceId$.subscribe( + (currentWorkspaceId) => (this.currentWorkspaceId = currentWorkspaceId) + ); + /** + * Add logic to check if current workspace id is still valid + * If not, remove the current workspace id and notify other subscribers + */ + this.workspaceList$.subscribe(async (workspaceList) => { + const currentWorkspaceId = this.currentWorkspaceId; + if (currentWorkspaceId) { + const findItem = workspaceList.find((item) => item.id === currentWorkspaceId); + if (!findItem) { + /** + * Current workspace is staled + */ + this.currentWorkspaceId$.next(''); + } + } + }); + + /** + * Initialize workspace list + */ + this.updateWorkspaceListAndNotify(); } + private catchedFetch = async >( + path: string, + options: HttpFetchOptions + ) => { + try { + return await this.http.fetch(path, options); + } catch (error: unknown) { + if (error instanceof HttpFetchError || error instanceof Error) { + return { + success: false, + error: error.message, + } as T; + } + + return { + success: false, + error: 'Unknown error', + } as T; + } + }; + private getPath(path: Array): string { return resolveUrl(`${WORKSPACES_API_BASE_URL}/`, join(...path)); } + private async updateWorkspaceListAndNotify(): Promise { + const result = await this.list({ + perPage: 999, + }); + + if (result?.success) { + this.workspaceList$.next(result.result.workspaces); + } + } + public async enterWorkspace(id: string): Promise> { - return this.http.post(this.getPath(['_enter', id])); + const workspaceResp = await this.get(id); + if (workspaceResp.success) { + this.currentWorkspaceId$.next(id); + return { + success: true, + result: null, + }; + } else { + return workspaceResp; + } } public async exitWorkspace(): Promise> { - return this.http.post(this.getPath(['_exit'])); + this.currentWorkspaceId$.next(''); + return { + success: true, + result: null, + }; } public async getCurrentWorkspaceId(): Promise> { - const currentWorkspaceIdResp = await this.http.get(this.getPath(['_current'])); - if (currentWorkspaceIdResp.success && !currentWorkspaceIdResp.result) { + const currentWorkspaceId = this.currentWorkspaceId; + if (!currentWorkspaceId) { return { success: false, error: 'You are not in any workspace yet.', }; } - return currentWorkspaceIdResp; + return { + success: true, + result: currentWorkspaceId, + }; } public async getCurrentWorkspace(): Promise> { @@ -83,22 +158,31 @@ export class WorkspacesClient { * @param attributes * @returns */ - public create = ( + public async create( attributes: Omit - ): Promise> => { + ): Promise> { if (!attributes) { - return Promise.reject(new Error('requires attributes')); + return { + success: false, + error: 'Workspace attributes is required', + }; } const path = this.getPath([]); - return this.http.fetch(path, { + const result = await this.catchedFetch>(path, { method: 'POST', body: JSON.stringify({ attributes, }), }); - }; + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } /** * Deletes a workspace @@ -106,13 +190,22 @@ export class WorkspacesClient { * @param id * @returns */ - public delete = (id: string): Promise> => { + public async delete(id: string): Promise> { if (!id) { - return Promise.reject(new Error('requires id')); + return { + success: false, + error: 'Id is required.', + }; } - return this.http.delete(this.getPath([id]), { method: 'DELETE' }); - }; + const result = await this.catchedFetch(this.getPath([id]), { method: 'DELETE' }); + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } /** * Search for workspaces @@ -137,9 +230,9 @@ export class WorkspacesClient { }> > => { const path = this.getPath(['_list']); - return this.http.fetch(path, { - method: 'GET', - query: options, + return this.catchedFetch(path, { + method: 'POST', + body: JSON.stringify(options || {}), }); }; @@ -149,16 +242,19 @@ export class WorkspacesClient { * @param {string} id * @returns The workspace for the given id. */ - public get = (id: string): Promise> => { + public async get(id: string): Promise> { if (!id) { - return Promise.reject(new Error('requires id')); + return { + success: false, + error: 'Id is required.', + }; } const path = this.getPath([id]); - return this.http.fetch(path, { + return this.catchedFetch(path, { method: 'GET', }); - }; + } /** * Updates a workspace @@ -167,9 +263,15 @@ export class WorkspacesClient { * @param {object} attributes * @returns */ - public update(id: string, attributes: Partial): Promise> { + public async update( + id: string, + attributes: Partial + ): Promise> { if (!id || !attributes) { - return Promise.reject(new Error('requires id and attributes')); + return { + success: false, + error: 'Id and attributes are required.', + }; } const path = this.getPath([id]); @@ -177,9 +279,20 @@ export class WorkspacesClient { attributes, }; - return this.http.fetch(path, { + const result = await this.catchedFetch(path, { method: 'PUT', body: JSON.stringify(body), }); + + if (result.success) { + this.updateWorkspaceListAndNotify(); + } + + return result; + } + + public stop() { + this.workspaceList$.unsubscribe(); + this.currentWorkspaceId$.unsubscribe(); } } diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index f691ebbe3e16..908530885760 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -4,19 +4,56 @@ */ import { CoreService } from 'src/core/types'; import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client'; -import { HttpStart } from '..'; +import type { WorkspaceAttribute } from '../../server/types'; +import { WORKSPACE_ID_QUERYSTRING_NAME } from './consts'; +import { HttpSetup } from '../http'; /** * @public */ export interface WorkspacesStart { client: WorkspacesClientContract; + formatUrlWithWorkspaceId: (url: string, id: WorkspaceAttribute['id']) => string; } -export class WorkspacesService implements CoreService { - public async setup() {} - public async start({ http }: { http: HttpStart }): Promise { - return { client: new WorkspacesClient(http) }; +export type WorkspacesSetup = WorkspacesStart; + +function setQuerystring(url: string, params: Record): string { + const urlObj = new URL(url); + const searchParams = new URLSearchParams(urlObj.search); + + for (const key in params) { + if (params.hasOwnProperty(key)) { + const value = params[key]; + searchParams.set(key, value); + } + } + + urlObj.search = searchParams.toString(); + return urlObj.toString(); +} + +export class WorkspacesService implements CoreService { + private client?: WorkspacesClientContract; + private formatUrlWithWorkspaceId(url: string, id: string) { + return setQuerystring(url, { + [WORKSPACE_ID_QUERYSTRING_NAME]: id, + }); + } + public async setup({ http }: { http: HttpSetup }) { + this.client = new WorkspacesClient(http); + return { + client: this.client, + formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + }; + } + public async start(): Promise { + return { + client: this.client as WorkspacesClientContract, + formatUrlWithWorkspaceId: this.formatUrlWithWorkspaceId, + }; + } + public async stop() { + this.client?.stop(); } - public async stop() {} } diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index c489d98cf708..b248a67ef50c 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -520,7 +520,7 @@ describe('http service', () => { }); const coreStart = await root.start(); - opensearch = coreStart.opensearch; + opensearch = coreStart?.opensearch; const { header } = await osdTestServer.request.get(root, '/new-platform/').expect(401); @@ -556,7 +556,7 @@ describe('http service', () => { }); const coreStart = await root.start(); - opensearch = coreStart.opensearch; + opensearch = coreStart?.opensearch; const { header } = await osdTestServer.request.get(root, '/new-platform/').expect(401); diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 0eb0b684d4f4..2b4df7da68bf 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -48,7 +48,6 @@ import { InternalStatusServiceSetup } from './status'; import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; -import { InternalWorkspacesServiceSetup, InternalWorkspacesServiceStart } from './workspaces'; /** @internal */ export interface InternalCoreSetup { @@ -65,7 +64,6 @@ export interface InternalCoreSetup { auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; - workspaces: InternalWorkspacesServiceSetup; } /** @@ -80,7 +78,6 @@ export interface InternalCoreStart { uiSettings: InternalUiSettingsServiceStart; auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; - workspaces: InternalWorkspacesServiceStart; } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 99c79351d861..f80b90ba6baa 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -175,7 +175,7 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); - const workspacesSetup = await this.workspaces.setup({ + await this.workspaces.setup({ http: httpSetup, savedObject: savedObjectsSetup, }); @@ -220,7 +220,6 @@ export class Server { auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, - workspaces: workspacesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -262,7 +261,7 @@ export class Server { opensearch: opensearchStart, savedObjects: savedObjectsStart, }); - const workspacesStart = await this.workspaces.start({ + await this.workspaces.start({ savedObjects: savedObjectsStart, }); @@ -275,7 +274,6 @@ export class Server { uiSettings: uiSettingsStart, auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, - workspaces: workspacesStart, }; const pluginsStart = await this.plugins.start(this.coreStart); diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts index 838f216bbd86..b9f765e4bba3 100644 --- a/src/core/server/workspaces/index.ts +++ b/src/core/server/workspaces/index.ts @@ -10,4 +10,4 @@ export { InternalWorkspacesServiceStart, } from './workspaces_service'; -export { WorkspaceAttribute, WorkspaceFindOptions, WORKSPACES_API_BASE_URL } from './types'; +export { WorkspaceAttribute, WorkspaceFindOptions } from './types'; diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts index 6a0422e2aa30..24345b6a34d9 100644 --- a/src/core/server/workspaces/routes/index.ts +++ b/src/core/server/workspaces/routes/index.ts @@ -5,13 +5,11 @@ import { schema } from '@osd/config-schema'; import { InternalHttpServiceSetup } from '../../http'; import { Logger } from '../../logging'; -import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL, WORKSPACE_ID_COOKIE_NAME } from '../types'; +import { IWorkspaceDBImpl } from '../types'; -function getCookieValue(cookieString: string, cookieName: string): string | null { - const regex = new RegExp(`(?:(?:^|.*;\\s*)${cookieName}\\s*\\=\\s*([^;]*).*$)|^.*$`); - const match = cookieString.match(regex); - return match ? match[1] : null; -} +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +export const WORKSPACE_ID_QUERYSTRING_NAME = '_workspace_id_'; export function registerRoutes({ client, @@ -23,15 +21,17 @@ export function registerRoutes({ http: InternalHttpServiceSetup; }) { const router = http.createRouter(WORKSPACES_API_BASE_URL); - router.get( + router.post( { path: '/_list', validate: { - query: schema.object({ - per_page: schema.number({ min: 0, defaultValue: 20 }), + body: schema.object({ + search: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.string()), + perPage: schema.number({ min: 0, defaultValue: 20 }), page: schema.number({ min: 0, defaultValue: 1 }), - sort_field: schema.maybe(schema.string()), - fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + sortField: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), }), }, }, @@ -42,7 +42,7 @@ export function registerRoutes({ request: req, logger, }, - req.query + req.body ); return res.ok({ body: result }); }) @@ -71,12 +71,13 @@ export function registerRoutes({ ); router.post( { - path: '/{id?}', + path: '/', validate: { body: schema.object({ attributes: schema.object({ description: schema.maybe(schema.string()), name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), }), }), }, @@ -106,6 +107,7 @@ export function registerRoutes({ attributes: schema.object({ description: schema.maybe(schema.string()), name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), }), }), }, @@ -149,72 +151,4 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); - router.post( - { - path: '/_enter/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - router.handleLegacyErrors(async (context, req, res) => { - const { id } = req.params; - - const result = await client.get( - { - context, - request: req, - logger, - }, - id - ); - if (result.success) { - return res.custom({ - body: { - success: true, - }, - statusCode: 200, - headers: { - 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=${id}; Path=/`, - }, - }); - } else { - return res.ok({ body: result }); - } - }) - ); - - router.post( - { - path: '/_exit', - validate: {}, - }, - router.handleLegacyErrors(async (context, req, res) => { - return res.custom({ - body: { - success: true, - }, - statusCode: 200, - headers: { - 'set-cookie': `${WORKSPACE_ID_COOKIE_NAME}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/`, - }, - }); - }) - ); - - router.get( - { - path: '/_current', - validate: {}, - }, - router.handleLegacyErrors(async (context, req, res) => { - return res.ok({ - body: { - success: true, - result: getCookieValue(req.headers.cookie as string, WORKSPACE_ID_COOKIE_NAME), - }, - }); - }) - ); } diff --git a/src/core/server/workspaces/saved_objects/workspace.ts b/src/core/server/workspaces/saved_objects/workspace.ts index d211aaa3ea93..e3fbaa0dad6a 100644 --- a/src/core/server/workspaces/saved_objects/workspace.ts +++ b/src/core/server/workspaces/saved_objects/workspace.ts @@ -29,8 +29,8 @@ export const workspace: SavedObjectsType = { mappings: { dynamic: false, properties: { - title: { - type: 'text', + name: { + type: 'keyword', }, description: { type: 'text', diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts index fc3cae861c77..e098b4905a1f 100644 --- a/src/core/server/workspaces/types.ts +++ b/src/core/server/workspaces/types.ts @@ -19,11 +19,11 @@ export interface WorkspaceAttribute { export interface WorkspaceFindOptions { page?: number; - per_page?: number; + perPage?: number; search?: string; - search_fields?: string[]; - sort_field?: string; - sort_order?: string; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; } export interface IRequestDetail { @@ -67,7 +67,3 @@ export type IResponse = success: false; error?: string; }; - -export const WORKSPACES_API_BASE_URL = '/api/workspaces'; - -export const WORKSPACE_ID_COOKIE_NAME = 'trinity_workspace_id'; diff --git a/src/core/server/workspaces/workspaces_client_with_saved_object.ts b/src/core/server/workspaces/workspaces_client.ts similarity index 100% rename from src/core/server/workspaces/workspaces_client_with_saved_object.ts rename to src/core/server/workspaces/workspaces_client.ts diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts index 15c150b7378a..6faec9a6496e 100644 --- a/src/core/server/workspaces/workspaces_service.ts +++ b/src/core/server/workspaces/workspaces_service.ts @@ -12,10 +12,10 @@ import { InternalSavedObjectsServiceStart, } from '../saved_objects'; import { IWorkspaceDBImpl } from './types'; -import { WorkspacesClientWithSavedObject } from './workspaces_client_with_saved_object'; +import { WorkspacesClientWithSavedObject } from './workspaces_client'; export interface WorkspacesServiceSetup { - setWorkspacesClient: (client: IWorkspaceDBImpl) => void; + client: IWorkspaceDBImpl; } export interface WorkspacesServiceStart { @@ -39,14 +39,14 @@ export class WorkspacesService implements CoreService { private logger: Logger; private client?: IWorkspaceDBImpl; - constructor(private readonly coreContext: CoreContext) { + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('workspaces-service'); } public async setup(setupDeps: WorkspacesSetupDeps): Promise { this.logger.debug('Setting up Workspaces service'); - this.client = this.client || new WorkspacesClientWithSavedObject(setupDeps); + this.client = new WorkspacesClientWithSavedObject(setupDeps); await this.client.setup(setupDeps); registerRoutes({ @@ -56,9 +56,7 @@ export class WorkspacesService }); return { - setWorkspacesClient: (client: IWorkspaceDBImpl) => { - this.client = client; - }, + client: this.client, }; } diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 5ccad2c6a2a9..4ac1575c25f7 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -5,3 +5,4 @@ export const WORKSPACE_APP_ID = 'workspace'; export const WORKSPACE_APP_NAME = 'Workspace'; +export const WORKSPACE_ID_IN_SESSION_STORAGE = '_workspace_id_'; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 13017dab8835..911c9a651137 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -4,6 +4,7 @@ */ import { i18n } from '@osd/i18n'; +import { parse } from 'querystring'; import { CoreSetup, CoreStart, @@ -11,10 +12,71 @@ import { AppMountParameters, AppNavLinkStatus, } from '../../../core/public'; -import { WORKSPACE_APP_ID } from '../common/constants'; +import { WORKSPACE_APP_ID, WORKSPACE_ID_IN_SESSION_STORAGE } from '../common/constants'; +import { WORKSPACE_ID_QUERYSTRING_NAME } from '../../../core/public'; export class WorkspacesPlugin implements Plugin<{}, {}> { - public setup(core: CoreSetup) { + private core?: CoreSetup; + private addWorkspaceListener() { + this.core?.workspaces.client.currentWorkspaceId$.subscribe((newWorkspaceId) => { + try { + sessionStorage.setItem(WORKSPACE_ID_IN_SESSION_STORAGE, newWorkspaceId); + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + } + }); + } + private getWorkpsaceIdFromQueryString(): string { + const querystringObject = parse(window.location.search.replace(/^\??/, '')); + return querystringObject[WORKSPACE_ID_QUERYSTRING_NAME] as string; + } + private getWorkpsaceIdFromSessionStorage(): string { + try { + return sessionStorage.getItem(WORKSPACE_ID_IN_SESSION_STORAGE) || ''; + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + return ''; + } + } + private clearWorkspaceIdFromSessionStorage(): void { + try { + sessionStorage.removeItem(WORKSPACE_ID_IN_SESSION_STORAGE); + } catch (e) { + /** + * in incognize mode, this method may throw an error + * */ + } + } + public async setup(core: CoreSetup) { + this.core = core; + /** + * register a listener + */ + this.addWorkspaceListener(); + + /** + * Retrive workspace id from url or sessionstorage + * url > sessionstorage + */ + const workspaceId = + this.getWorkpsaceIdFromQueryString() || this.getWorkpsaceIdFromSessionStorage(); + + if (workspaceId) { + const result = await core.workspaces.client.enterWorkspace(workspaceId); + if (!result.success) { + this.clearWorkspaceIdFromSessionStorage(); + core.fatalErrors.add( + result.error || + i18n.translate('workspace.error.setup', { + defaultMessage: 'Workspace init failed', + }) + ); + } + } core.application.register({ id: WORKSPACE_APP_ID, title: i18n.translate('workspace.settings.title', {