Skip to content

Commit

Permalink
Feature: create management / public workspaces when calling list api (#…
Browse files Browse the repository at this point in the history
…236)

* feat: create management / public workspaces when calling list api

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: fix bootstrap

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* fix: integration test

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* fix: flaky test

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

---------

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe committed Mar 18, 2024
1 parent 7185d73 commit 0d079d7
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 11 deletions.
8 changes: 7 additions & 1 deletion src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/core/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 7 additions & 1 deletion src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/workspace/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
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);

Expand Down
127 changes: 122 additions & 5 deletions src/plugins/workspace/server/workspace_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,41 @@ 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;

const DUPLICATE_WORKSPACE_NAME_ERROR = i18n.translate('workspace.duplicate.name.error', {
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(
Expand Down Expand Up @@ -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<WorkspaceAttribute, 'id' | 'permissions'>
) {
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<IResponse<boolean>> {
this.setupDep.savedObjects.registerType(workspace);
return {
Expand Down Expand Up @@ -108,7 +174,7 @@ export class WorkspaceClient implements IWorkspaceClientImpl {
options: WorkspaceFindOptions
): ReturnType<IWorkspaceClientImpl['list']> {
try {
const {
let {
saved_objects: savedObjects,
...others
} = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find<WorkspaceAttribute>(
Expand All @@ -117,6 +183,49 @@ export class WorkspaceClient implements IWorkspaceClientImpl {
type: WORKSPACE_TYPE,
}
);
const scopedClientWithoutPermissionCheck = this.getScopedClientWithoutPermission(
requestDetail
);
const tasks: Array<Promise<unknown>> = [];

/**
* 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: {
Expand Down Expand Up @@ -160,6 +269,9 @@ export class WorkspaceClient implements IWorkspaceClientImpl {
const client = this.getSavedObjectClientsFromRequestDetail(requestDetail);
const workspaceInDB: SavedObject<WorkspaceAttribute> = 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({
Expand All @@ -172,7 +284,12 @@ export class WorkspaceClient implements IWorkspaceClientImpl {
throw new Error(DUPLICATE_WORKSPACE_NAME_ERROR);
}
}
await client.update<Omit<WorkspaceAttribute, 'id'>>(WORKSPACE_TYPE, id, attributes, {});

await client.create<Omit<WorkspaceAttribute, 'id'>>(WORKSPACE_TYPE, attributes, {
id,
overwrite: true,
version: workspaceInDB.version,
});
return {
success: true,
result: true,
Expand Down

0 comments on commit 0d079d7

Please sign in to comment.