diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d3df420710e5..736f12ab28fe 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -349,7 +349,13 @@ export { } from './metrics'; export { AppCategory, WorkspaceAttribute } from '../types'; -export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE } from '../utils'; +export { + DEFAULT_APP_CATEGORIES, + PUBLIC_WORKSPACE_ID, + MANAGEMENT_WORKSPACE_ID, + WORKSPACE_TYPE, + PERSONAL_WORKSPACE_ID_PREFIX, +} from '../utils'; export { SavedObject, diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts index ecc1b7e863c4..0993f0587e28 100644 --- a/src/core/utils/constants.ts +++ b/src/core/utils/constants.ts @@ -6,3 +6,9 @@ export const WORKSPACE_TYPE = 'workspace'; export const WORKSPACE_PATH_PREFIX = '/w'; + +export const PUBLIC_WORKSPACE_ID = 'public'; + +export const MANAGEMENT_WORKSPACE_ID = 'management'; + +export const PERSONAL_WORKSPACE_ID_PREFIX = 'personal'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index a83f85a8fce0..a4b6cd4a922b 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,5 +37,11 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; -export { WORKSPACE_PATH_PREFIX, WORKSPACE_TYPE } from './constants'; export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; +export { + WORKSPACE_PATH_PREFIX, + PUBLIC_WORKSPACE_ID, + MANAGEMENT_WORKSPACE_ID, + WORKSPACE_TYPE, + PERSONAL_WORKSPACE_ID_PREFIX, +} from './constants'; diff --git a/src/plugins/console/public/application/components/import_flyout.test.tsx b/src/plugins/console/public/application/components/import_flyout.test.tsx index 5e7093fe306c..f04678155405 100644 --- a/src/plugins/console/public/application/components/import_flyout.test.tsx +++ b/src/plugins/console/public/application/components/import_flyout.test.tsx @@ -10,6 +10,7 @@ import { ContextValue, ServicesContextProvider } from '../contexts'; import { serviceContextMock } from '../contexts/services_context.mock'; import { wrapWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { ReactWrapper, mount } from 'enzyme'; +import { waitFor } from '@testing-library/dom'; const mockFile = new File(['{"text":"Sample JSON data"}'], 'sample.json', { type: 'application/json', @@ -122,9 +123,11 @@ describe('ImportFlyout Component', () => { expect(component.find(overwriteOptionIdentifier).first().props().checked).toBe(false); // should update existing query - expect(mockUpdate).toBeCalledTimes(1); - expect(mockClose).toBeCalledTimes(1); - expect(mockRefresh).toBeCalledTimes(1); + await waitFor(() => { + expect(mockUpdate).toBeCalledTimes(1); + expect(mockClose).toBeCalledTimes(1); + expect(mockRefresh).toBeCalledTimes(1); + }); }); it('should handle errors during import', async () => { diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 39a0769b4852..a88f98c03a47 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +export const WORKSPACE_UPDATE_APP_ID = 'workspace_update'; export const WORKSPACE_OVERVIEW_APP_ID = 'workspace_overview'; export const WORKSPACE_FATAL_ERROR_APP_ID = 'workspace_fatal_error'; export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; diff --git a/src/plugins/workspace/server/integration_tests/routes.test.ts b/src/plugins/workspace/server/integration_tests/routes.test.ts index a36d187b405f..82eff8c317c5 100644 --- a/src/plugins/workspace/server/integration_tests/routes.test.ts +++ b/src/plugins/workspace/server/integration_tests/routes.test.ts @@ -155,7 +155,7 @@ describe('workspace service', () => { const result: any = await osdTestServer.request .post(root, `/api/workspaces`) .send({ - attributes: omit(reservedWorkspace, 'id'), + attributes: omitId(reservedWorkspace), }) .expect(200); diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 739b10d8bc24..598bc0ad3d74 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -42,7 +42,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { public async setup(core: CoreSetup) { this.logger.debug('Setting up Workspaces service'); - this.client = new WorkspaceClient(core); + this.client = new WorkspaceClient(core, this.logger); await this.client.setup(core); diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index 092f3e528d4f..a52b74cc6111 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -11,11 +11,21 @@ import type { WorkspaceAttribute, SavedObjectsServiceStart, } from '../../../core/server'; -import { WORKSPACE_TYPE } from '../../../core/server'; +import { + DEFAULT_APP_CATEGORIES, + MANAGEMENT_WORKSPACE_ID, + PUBLIC_WORKSPACE_ID, + WORKSPACE_TYPE, + Logger, +} from '../../../core/server'; import { IWorkspaceClientImpl, WorkspaceFindOptions, IResponse, IRequestDetail } from './types'; import { workspace } from './saved_objects'; import { generateRandomId } from './utils'; -import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../common/constants'; +import { + WORKSPACE_OVERVIEW_APP_ID, + WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID, + WORKSPACE_UPDATE_APP_ID, +} from '../common/constants'; const WORKSPACE_ID_SIZE = 6; @@ -23,12 +33,19 @@ const DUPLICATE_WORKSPACE_NAME_ERROR = i18n.translate('workspace.duplicate.name. defaultMessage: 'workspace name has already been used, try with a different name', }); +const RESERVED_WORKSPACE_NAME_ERROR = i18n.translate('workspace.reserved.name.error', { + defaultMessage: 'reserved workspace name cannot be changed', +}); + export class WorkspaceClient implements IWorkspaceClientImpl { private setupDep: CoreSetup; + private logger: Logger; + private savedObjects?: SavedObjectsServiceStart; - constructor(core: CoreSetup) { + constructor(core: CoreSetup, logger: Logger) { this.setupDep = core; + this.logger = logger; } private getScopedClientWithoutPermission( @@ -58,6 +75,55 @@ export class WorkspaceClient implements IWorkspaceClientImpl { private formatError(error: Error | any): string { return error.message || error.error || 'Error'; } + private async checkAndCreateWorkspace( + savedObjectClient: SavedObjectsClientContract | undefined, + workspaceId: string, + workspaceAttribute: Omit + ) { + try { + await savedObjectClient?.get(WORKSPACE_TYPE, workspaceId); + } catch (error) { + this.logger.debug(error?.toString() || ''); + this.logger.info(`Workspace ${workspaceId} is not found, create it by using internal user`); + try { + const createResult = await savedObjectClient?.create(WORKSPACE_TYPE, workspaceAttribute, { + id: workspaceId, + }); + if (createResult?.id) { + this.logger.info(`Created workspace ${createResult.id}.`); + } + } catch (e) { + this.logger.error(`Create ${workspaceId} workspace error: ${e?.toString() || ''}`); + } + } + } + private async setupPublicWorkspace(savedObjectClient?: SavedObjectsClientContract) { + return this.checkAndCreateWorkspace(savedObjectClient, PUBLIC_WORKSPACE_ID, { + name: i18n.translate('workspaces.public.workspace.default.name', { + defaultMessage: 'Global workspace', + }), + features: ['*', `!@${DEFAULT_APP_CATEGORIES.management.id}`], + reserved: true, + }); + } + private async setupManagementWorkspace(savedObjectClient?: SavedObjectsClientContract) { + const DSM_APP_ID = 'dataSources'; + const DEV_TOOLS_APP_ID = 'dev_tools'; + + return this.checkAndCreateWorkspace(savedObjectClient, MANAGEMENT_WORKSPACE_ID, { + name: i18n.translate('workspaces.management.workspace.default.name', { + defaultMessage: 'Management', + }), + features: [ + `@${DEFAULT_APP_CATEGORIES.management.id}`, + WORKSPACE_OVERVIEW_APP_ID, + WORKSPACE_UPDATE_APP_ID, + DSM_APP_ID, + DEV_TOOLS_APP_ID, + ], + reserved: true, + }); + } public async setup(core: CoreSetup): Promise> { this.setupDep.savedObjects.registerType(workspace); return { @@ -108,7 +174,7 @@ export class WorkspaceClient implements IWorkspaceClientImpl { options: WorkspaceFindOptions ): ReturnType { try { - const { + let { saved_objects: savedObjects, ...others } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( @@ -117,6 +183,49 @@ export class WorkspaceClient implements IWorkspaceClientImpl { type: WORKSPACE_TYPE, } ); + const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission( + requestDetail + ); + const tasks: Array> = []; + + /** + * Setup public workspace if public workspace can not be found + */ + const hasPublicWorkspace = savedObjects.some((item) => item.id === PUBLIC_WORKSPACE_ID); + + if (!hasPublicWorkspace) { + tasks.push(this.setupPublicWorkspace(scopedClientWithoutPermissionCheck)); + } + + /** + * Setup management workspace if management workspace can not be found + */ + const hasManagementWorkspace = savedObjects.some( + (item) => item.id === MANAGEMENT_WORKSPACE_ID + ); + if (!hasManagementWorkspace) { + tasks.push(this.setupManagementWorkspace(scopedClientWithoutPermissionCheck)); + } + + try { + await Promise.all(tasks); + if (tasks.length) { + const { + saved_objects: retryFindSavedObjects, + ...retryFindOthers + } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find< + WorkspaceAttribute + >({ + ...options, + type: WORKSPACE_TYPE, + }); + savedObjects = retryFindSavedObjects; + others = retryFindOthers; + } + } catch (e) { + this.logger.error(`Some error happened when initializing reserved workspace: ${e}`); + } + return { success: true, result: { @@ -160,6 +269,9 @@ export class WorkspaceClient implements IWorkspaceClientImpl { const client = this.getSavedObjectClientsFromRequestDetail(requestDetail); const workspaceInDB: SavedObject = await client.get(WORKSPACE_TYPE, id); if (workspaceInDB.attributes.name !== attributes.name) { + if (workspaceInDB.attributes.reserved) { + throw new Error(RESERVED_WORKSPACE_NAME_ERROR); + } const existingWorkspaceRes = await this.getScopedClientWithoutPermission( requestDetail )?.find({ @@ -172,7 +284,12 @@ export class WorkspaceClient implements IWorkspaceClientImpl { throw new Error(DUPLICATE_WORKSPACE_NAME_ERROR); } } - await client.update>(WORKSPACE_TYPE, id, attributes, {}); + + await client.create>(WORKSPACE_TYPE, attributes, { + id, + overwrite: true, + version: workspaceInDB.version, + }); return { success: true, result: true,