From 074d23be87eb6d5952b7d3119181f26d5c87cb39 Mon Sep 17 00:00:00 2001 From: SuZhou-Joe Date: Mon, 11 Sep 2023 10:49:44 +0800 Subject: [PATCH 1/4] [Feature] Setup workspace skeleton and implement basic CRUD API (#130) * feature: setup workspace skeleton and implement basic CRUD API on workspace Signed-off-by: Zhou Su * feat: remove useless required plugins and logger typo Signed-off-by: SuZhou-Joe * feat: setup public side skeleton Signed-off-by: SuZhou-Joe --------- Signed-off-by: Zhou Su Signed-off-by: SuZhou-Joe Co-authored-by: Zhou Su --- src/core/server/index.ts | 4 +- src/core/types/index.ts | 1 + src/core/types/workspace.ts | 14 ++ src/core/utils/constants.ts | 6 + src/core/utils/index.ts | 1 + .../workspace/opensearch_dashboards.json | 9 + src/plugins/workspace/public/index.ts | 10 + src/plugins/workspace/public/plugin.ts | 17 ++ src/plugins/workspace/server/index.ts | 13 ++ src/plugins/workspace/server/plugin.ts | 45 ++++ src/plugins/workspace/server/routes/index.ts | 181 ++++++++++++++++ .../workspace/server/saved_objects/index.ts | 6 + .../server/saved_objects/workspace.ts | 41 ++++ src/plugins/workspace/server/types.ts | 65 ++++++ src/plugins/workspace/server/utils.test.ts | 21 ++ src/plugins/workspace/server/utils.ts | 13 ++ .../workspace/server/workspace_client.ts | 193 ++++++++++++++++++ 17 files changed, 638 insertions(+), 2 deletions(-) create mode 100644 src/core/types/workspace.ts create mode 100644 src/core/utils/constants.ts create mode 100644 src/plugins/workspace/opensearch_dashboards.json create mode 100644 src/plugins/workspace/public/index.ts create mode 100644 src/plugins/workspace/public/plugin.ts create mode 100644 src/plugins/workspace/server/index.ts create mode 100644 src/plugins/workspace/server/plugin.ts create mode 100644 src/plugins/workspace/server/routes/index.ts create mode 100644 src/plugins/workspace/server/saved_objects/index.ts create mode 100644 src/plugins/workspace/server/saved_objects/workspace.ts create mode 100644 src/plugins/workspace/server/types.ts create mode 100644 src/plugins/workspace/server/utils.test.ts create mode 100644 src/plugins/workspace/server/utils.ts create mode 100644 src/plugins/workspace/server/workspace_client.ts diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ca55cc8dc1d5..379411398fca 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -345,8 +345,8 @@ export { MetricsServiceStart, } from './metrics'; -export { AppCategory } from '../types'; -export { DEFAULT_APP_CATEGORIES } from '../utils'; +export { AppCategory, WorkspaceAttribute } from '../types'; +export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE } from '../utils'; export { SavedObject, diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 9f620273e3b2..4afe9c537f75 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -39,3 +39,4 @@ export * from './ui_settings'; export * from './saved_objects'; export * from './serializable'; export * from './custom_branding'; +export * from './workspace'; diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts new file mode 100644 index 000000000000..23c3b2038ff2 --- /dev/null +++ b/src/core/types/workspace.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface WorkspaceAttribute { + id: string; + name: string; + description?: string; + features?: string[]; + color?: string; + icon?: string; + defaultVISTheme?: string; +} diff --git a/src/core/utils/constants.ts b/src/core/utils/constants.ts new file mode 100644 index 000000000000..73c2d6010846 --- /dev/null +++ b/src/core/utils/constants.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const WORKSPACE_TYPE = 'workspace'; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index b3b6ce4aab02..af4f9a17ae58 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -37,3 +37,4 @@ export { IContextProvider, } from './context'; export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; +export { WORKSPACE_TYPE } from './constants'; diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json new file mode 100644 index 000000000000..ea2fe1cbed49 --- /dev/null +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -0,0 +1,9 @@ +{ + "id": "workspace", + "version": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/src/plugins/workspace/public/index.ts b/src/plugins/workspace/public/index.ts new file mode 100644 index 000000000000..99161a7edbd7 --- /dev/null +++ b/src/plugins/workspace/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WorkspacePlugin } from './plugin'; + +export function plugin() { + return new WorkspacePlugin(); +} diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts new file mode 100644 index 000000000000..933afa5858be --- /dev/null +++ b/src/plugins/workspace/public/plugin.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Plugin } from '../../../core/public'; + +export class WorkspacePlugin implements Plugin<{}, {}, {}> { + public async setup() { + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/workspace/server/index.ts b/src/plugins/workspace/server/index.ts new file mode 100644 index 000000000000..936d3cf3ecec --- /dev/null +++ b/src/plugins/workspace/server/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { PluginInitializerContext } from '../../../core/server'; +import { WorkspacePlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WorkspacePlugin(initializerContext); +} diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts new file mode 100644 index 000000000000..515ccc399f8e --- /dev/null +++ b/src/plugins/workspace/server/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { PluginInitializerContext, CoreSetup, Plugin, Logger } from '../../../core/server'; +import { IWorkspaceDBImpl } from './types'; +import { WorkspaceClientWithSavedObject } from './workspace_client'; +import { registerRoutes } from './routes'; + +export class WorkspacePlugin implements Plugin<{}, {}> { + private readonly logger: Logger; + private client?: IWorkspaceDBImpl; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get('plugins', 'workspace'); + } + + public async setup(core: CoreSetup) { + this.logger.debug('Setting up Workspaces service'); + + this.client = new WorkspaceClientWithSavedObject(core); + + await this.client.setup(core); + + registerRoutes({ + http: core.http, + logger: this.logger, + client: this.client as IWorkspaceDBImpl, + }); + + return { + client: this.client, + }; + } + + public start() { + this.logger.debug('Starting Workspace service'); + + return { + client: this.client as IWorkspaceDBImpl, + }; + } + + public stop() {} +} diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts new file mode 100644 index 000000000000..39cc5877b469 --- /dev/null +++ b/src/plugins/workspace/server/routes/index.ts @@ -0,0 +1,181 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { schema } from '@osd/config-schema'; + +import { CoreSetup, Logger } from '../../../../core/server'; +import { IWorkspaceDBImpl } from '../types'; + +const WORKSPACES_API_BASE_URL = '/api/workspaces'; + +const workspaceAttributesSchema = schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + features: schema.maybe(schema.arrayOf(schema.string())), + color: schema.maybe(schema.string()), + icon: schema.maybe(schema.string()), + defaultVISTheme: schema.maybe(schema.string()), +}); + +export function registerRoutes({ + client, + logger, + http, +}: { + client: IWorkspaceDBImpl; + logger: Logger; + http: CoreSetup['http']; +}) { + const router = http.createRouter(); + router.post( + { + path: `${WORKSPACES_API_BASE_URL}/_list`, + validate: { + 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 }), + sortField: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const result = await client.list( + { + context, + request: req, + logger, + }, + req.body + ); + if (!result.success) { + return res.ok({ body: result }); + } + return res.ok({ + body: { + ...result, + result: { + ...result.result, + workspaces: result.result.workspaces.map((workspace) => ({ + ...workspace, + })), + }, + }, + }); + }) + ); + router.get( + { + path: `${WORKSPACES_API_BASE_URL}/{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.ok({ body: result }); + } + + return res.ok({ + body: { + ...result, + result: { + ...result.result, + }, + }, + }); + }) + ); + router.post( + { + path: `${WORKSPACES_API_BASE_URL}`, + validate: { + body: schema.object({ + attributes: workspaceAttributesSchema, + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { attributes } = req.body; + + const result = await client.create( + { + context, + request: req, + logger, + }, + { + ...attributes, + } + ); + return res.ok({ body: result }); + }) + ); + router.put( + { + path: `${WORKSPACES_API_BASE_URL}/{id?}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + attributes: workspaceAttributesSchema, + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + const { attributes } = req.body; + + const result = await client.update( + { + context, + request: req, + logger, + }, + id, + { + ...attributes, + } + ); + return res.ok({ body: result }); + }) + ); + router.delete( + { + path: `${WORKSPACES_API_BASE_URL}/{id?}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + + const result = await client.delete( + { + context, + request: req, + logger, + }, + id + ); + return res.ok({ body: result }); + }) + ); +} diff --git a/src/plugins/workspace/server/saved_objects/index.ts b/src/plugins/workspace/server/saved_objects/index.ts new file mode 100644 index 000000000000..51653c50681e --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { workspace } from './workspace'; diff --git a/src/plugins/workspace/server/saved_objects/workspace.ts b/src/plugins/workspace/server/saved_objects/workspace.ts new file mode 100644 index 000000000000..5142185b0c2d --- /dev/null +++ b/src/plugins/workspace/server/saved_objects/workspace.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType, WORKSPACE_TYPE } from '../../../../core/server'; + +export const workspace: SavedObjectsType = { + name: WORKSPACE_TYPE, + namespaceType: 'agnostic', + hidden: false, + /** + * workspace won't appear in management page. + */ + mappings: { + dynamic: false, + properties: { + name: { + type: 'keyword', + }, + description: { + type: 'text', + }, + /** + * In opensearch, string[] is also mapped to text + */ + features: { + type: 'text', + }, + color: { + type: 'text', + }, + icon: { + type: 'text', + }, + defaultVISTheme: { + type: 'text', + }, + }, + }, +}; diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts new file mode 100644 index 000000000000..d57cecd4a3c9 --- /dev/null +++ b/src/plugins/workspace/server/types.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + Logger, + OpenSearchDashboardsRequest, + RequestHandlerContext, + SavedObjectsFindResponse, + CoreSetup, + WorkspaceAttribute, + ISavedObjectsRepository, +} from '../../../core/server'; + +export interface WorkspaceFindOptions { + page?: number; + perPage?: number; + search?: string; + searchFields?: string[]; + sortField?: string; + sortOrder?: string; +} + +export interface IRequestDetail { + request: OpenSearchDashboardsRequest; + context: RequestHandlerContext; + logger: Logger; +} + +export interface IWorkspaceDBImpl { + setup(dep: CoreSetup): Promise>; + setInternalRepository(repository: ISavedObjectsRepository): void; + create( + requestDetail: IRequestDetail, + payload: Omit + ): Promise>; + list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): Promise< + IResponse< + { + workspaces: WorkspaceAttribute[]; + } & Pick + > + >; + get(requestDetail: IRequestDetail, id: string): Promise>; + update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise>; + delete(requestDetail: IRequestDetail, id: string): Promise>; + destroy(): Promise>; +} + +export type IResponse = + | { + result: T; + success: true; + } + | { + success: false; + error?: string; + }; diff --git a/src/plugins/workspace/server/utils.test.ts b/src/plugins/workspace/server/utils.test.ts new file mode 100644 index 000000000000..119b8889f715 --- /dev/null +++ b/src/plugins/workspace/server/utils.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { generateRandomId } from './utils'; + +describe('workspace utils', () => { + it('should generate id with the specified size', () => { + expect(generateRandomId(6)).toHaveLength(6); + }); + + it('should generate random IDs', () => { + const NUM_OF_ID = 10000; + const ids = new Set(); + for (let i = 0; i < NUM_OF_ID; i++) { + ids.add(generateRandomId(6)); + } + expect(ids.size).toBe(NUM_OF_ID); + }); +}); diff --git a/src/plugins/workspace/server/utils.ts b/src/plugins/workspace/server/utils.ts new file mode 100644 index 000000000000..89bfabd52657 --- /dev/null +++ b/src/plugins/workspace/server/utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import crypto from 'crypto'; + +/** + * Generate URL friendly random ID + */ +export const generateRandomId = (size: number) => { + return crypto.randomBytes(size).toString('base64url').slice(0, size); +}; diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts new file mode 100644 index 000000000000..ec49daf83b6a --- /dev/null +++ b/src/plugins/workspace/server/workspace_client.ts @@ -0,0 +1,193 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { i18n } from '@osd/i18n'; +import type { + SavedObject, + SavedObjectsClientContract, + CoreSetup, + WorkspaceAttribute, + ISavedObjectsRepository, +} from '../../../core/server'; +import { WORKSPACE_TYPE } from '../../../core/server'; +import { IWorkspaceDBImpl, WorkspaceFindOptions, IResponse, IRequestDetail } from './types'; +import { workspace } from './saved_objects'; +import { generateRandomId } from './utils'; + +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', +}); + +export class WorkspaceClientWithSavedObject implements IWorkspaceDBImpl { + private setupDep: CoreSetup; + + private internalSavedObjectsRepository?: ISavedObjectsRepository; + setInternalRepository(repository: ISavedObjectsRepository) { + this.internalSavedObjectsRepository = repository; + } + + constructor(core: CoreSetup) { + this.setupDep = core; + } + private getSavedObjectClientsFromRequestDetail( + requestDetail: IRequestDetail + ): SavedObjectsClientContract { + return requestDetail.context.core.savedObjects.client; + } + private getFlattenedResultWithSavedObject( + savedObject: SavedObject + ): WorkspaceAttribute { + return { + ...savedObject.attributes, + id: savedObject.id, + }; + } + private formatError(error: Error | any): string { + return error.message || error.error || 'Error'; + } + public async setup(core: CoreSetup): Promise> { + this.setupDep.savedObjects.registerType(workspace); + return { + success: true, + result: true, + }; + } + public async create( + requestDetail: IRequestDetail, + payload: Omit + ): ReturnType { + try { + const attributes = payload; + const id = generateRandomId(WORKSPACE_ID_SIZE); + const client = this.getSavedObjectClientsFromRequestDetail(requestDetail); + const existingWorkspaceRes = await this.internalSavedObjectsRepository?.find({ + type: WORKSPACE_TYPE, + search: attributes.name, + searchFields: ['name'], + }); + if (existingWorkspaceRes && existingWorkspaceRes.total > 0) { + throw new Error(DUPLICATE_WORKSPACE_NAME_ERROR); + } + const result = await client.create>( + WORKSPACE_TYPE, + attributes, + { + id, + } + ); + return { + success: true, + result: { + id: result.id, + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): ReturnType { + try { + const { + saved_objects: savedObjects, + ...others + } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( + { + ...options, + type: WORKSPACE_TYPE, + } + ); + return { + success: true, + result: { + ...others, + workspaces: savedObjects.map((item) => this.getFlattenedResultWithSavedObject(item)), + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async get( + requestDetail: IRequestDetail, + id: string + ): Promise> { + try { + const result = await this.getSavedObjectClientsFromRequestDetail(requestDetail).get< + WorkspaceAttribute + >(WORKSPACE_TYPE, id); + return { + success: true, + result: this.getFlattenedResultWithSavedObject(result), + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise> { + const attributes = payload; + try { + const client = this.getSavedObjectClientsFromRequestDetail(requestDetail); + const workspaceInDB: SavedObject = await client.get(WORKSPACE_TYPE, id); + if (workspaceInDB.attributes.name !== attributes.name) { + const existingWorkspaceRes = await this.internalSavedObjectsRepository?.find({ + type: WORKSPACE_TYPE, + search: attributes.name, + searchFields: ['name'], + fields: ['_id'], + }); + if (existingWorkspaceRes && existingWorkspaceRes.total > 0) { + throw new Error(DUPLICATE_WORKSPACE_NAME_ERROR); + } + } + await client.update>(WORKSPACE_TYPE, id, attributes, {}); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async delete(requestDetail: IRequestDetail, id: string): Promise> { + try { + await this.getSavedObjectClientsFromRequestDetail(requestDetail).delete(WORKSPACE_TYPE, id); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async destroy(): Promise> { + return { + success: true, + result: true, + }; + } +} From f1eb76281e8fc101c9c47c35ba896f3550113cfd Mon Sep 17 00:00:00 2001 From: gaobinlong Date: Thu, 14 Sep 2023 17:46:53 +0800 Subject: [PATCH 2/4] [Workspace] Add ACL related functions for workspace (#146) * [Workspace] Add acl related functions for workspace Signed-off-by: gaobinlong * Minor change Signed-off-by: gaobinlong --------- Signed-off-by: gaobinlong --- .../build_active_mappings.test.ts.snap | 110 ++++++++ .../migrations/core/build_active_mappings.ts | 20 ++ .../migrations/core/index_migrator.test.ts | 105 ++++++++ ...pensearch_dashboards_migrator.test.ts.snap | 55 ++++ .../permission_control/acl.test.ts | 166 ++++++++++++ .../saved_objects/permission_control/acl.ts | 249 ++++++++++++++++++ 6 files changed, 705 insertions(+) create mode 100644 src/core/server/saved_objects/permission_control/acl.test.ts create mode 100644 src/core/server/saved_objects/permission_control/acl.ts diff --git a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap index f8ef47cae894..6f67893104e7 100644 --- a/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap +++ b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap @@ -10,6 +10,7 @@ Object { "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", + "permissions": "07c04cdd060494956fdddaa7ef86e8ac", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -36,6 +37,60 @@ Object { "originId": Object { "type": "keyword", }, + "permissions": Object { + "properties": Object { + "library_read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "library_write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "management": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + }, + }, "references": Object { "properties": Object { "id": Object { @@ -69,6 +124,7 @@ Object { "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", + "permissions": "07c04cdd060494956fdddaa7ef86e8ac", "references": "7997cf5a56cc02bdc9c93361bde732b0", "secondType": "72d57924f415fbadb3ee293b67d233ab", "thirdType": "510f1f0adb69830cf8a1c5ce2923ed82", @@ -99,6 +155,60 @@ Object { "originId": Object { "type": "keyword", }, + "permissions": Object { + "properties": Object { + "library_read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "library_write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "management": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + }, + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index bf377a13a42e..02dc13b2cd3f 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -36,6 +36,7 @@ import crypto from 'crypto'; import { cloneDeep, mapValues } from 'lodash'; import { IndexMapping, + SavedObjectsFieldMapping, SavedObjectsMappingProperties, SavedObjectsTypeMappingDefinitions, } from './../../mappings'; @@ -137,6 +138,16 @@ function findChangedProp(actual: any, expected: any) { * @returns {IndexMapping} */ function defaultMapping(): IndexMapping { + const principals: SavedObjectsFieldMapping = { + properties: { + users: { + type: 'keyword', + }, + groups: { + type: 'keyword', + }, + }, + }; return { dynamic: 'strict', properties: { @@ -175,6 +186,15 @@ function defaultMapping(): IndexMapping { }, }, }, + permissions: { + properties: { + read: principals, + write: principals, + management: principals, + library_read: principals, + library_write: principals, + }, + }, }, }; } diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 4bacfda3bd5a..70f96c2e4daf 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -79,6 +79,7 @@ describe('IndexMigrator', () => { namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', + permissions: '07c04cdd060494956fdddaa7ef86e8ac', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -92,6 +93,40 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + permissions: { + properties: { + library_read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + library_write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + management: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + }, + }, references: { type: 'nested', properties: { @@ -196,6 +231,7 @@ describe('IndexMigrator', () => { namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', + permissions: '07c04cdd060494956fdddaa7ef86e8ac', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -210,6 +246,40 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + permissions: { + properties: { + library_read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + library_write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + management: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + }, + }, references: { type: 'nested', properties: { @@ -257,6 +327,7 @@ describe('IndexMigrator', () => { namespace: '2f4316de49999235636386fe51dc06c1', namespaces: '2f4316de49999235636386fe51dc06c1', originId: '2f4316de49999235636386fe51dc06c1', + permissions: '07c04cdd060494956fdddaa7ef86e8ac', references: '7997cf5a56cc02bdc9c93361bde732b0', type: '2f4316de49999235636386fe51dc06c1', updated_at: '00da57df13e94e9d98437d13ace4bfe0', @@ -271,6 +342,40 @@ describe('IndexMigrator', () => { originId: { type: 'keyword' }, type: { type: 'keyword' }, updated_at: { type: 'date' }, + permissions: { + properties: { + library_read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + library_write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + management: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + read: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + write: { + properties: { + users: { type: 'keyword' }, + groups: { type: 'keyword' }, + }, + }, + }, + }, references: { type: 'nested', properties: { diff --git a/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap index baebb7848798..5e39af788d79 100644 --- a/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/opensearch_dashboards/__snapshots__/opensearch_dashboards_migrator.test.ts.snap @@ -10,6 +10,7 @@ Object { "namespace": "2f4316de49999235636386fe51dc06c1", "namespaces": "2f4316de49999235636386fe51dc06c1", "originId": "2f4316de49999235636386fe51dc06c1", + "permissions": "07c04cdd060494956fdddaa7ef86e8ac", "references": "7997cf5a56cc02bdc9c93361bde732b0", "type": "2f4316de49999235636386fe51dc06c1", "updated_at": "00da57df13e94e9d98437d13ace4bfe0", @@ -44,6 +45,60 @@ Object { "originId": Object { "type": "keyword", }, + "permissions": Object { + "properties": Object { + "library_read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "library_write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "management": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "read": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + "write": Object { + "properties": Object { + "groups": Object { + "type": "keyword", + }, + "users": Object { + "type": "keyword", + }, + }, + }, + }, + }, "references": Object { "properties": Object { "id": Object { diff --git a/src/core/server/saved_objects/permission_control/acl.test.ts b/src/core/server/saved_objects/permission_control/acl.test.ts new file mode 100644 index 000000000000..057d294c3637 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/acl.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Principals, Permissions, ACL } from './acl'; + +describe('SavedObjectTypeRegistry', () => { + it('test has permission', () => { + const principals: Principals = { + users: ['user1'], + groups: [], + }; + const permissions: Permissions = { + read: principals, + }; + const acl = new ACL(permissions); + expect( + acl.hasPermission(['read'], { + users: ['user1'], + groups: [], + }) + ).toEqual(true); + expect( + acl.hasPermission(['read'], { + users: ['user2'], + groups: [], + }) + ).toEqual(false); + }); + + it('test add permission', () => { + const acl = new ACL(); + const result1 = acl + .addPermission(['read'], { + users: ['user1'], + groups: [], + }) + .getPermissions(); + expect(result1?.read?.users).toEqual(['user1']); + + acl.resetPermissions(); + const result2 = acl + .addPermission(['write', 'management'], { + users: ['user2'], + groups: ['group1', 'group2'], + }) + .getPermissions(); + expect(result2?.write?.users).toEqual(['user2']); + expect(result2?.management?.groups).toEqual(['group1', 'group2']); + }); + + it('test remove permission', () => { + const principals1: Principals = { + users: ['user1'], + groups: ['group1', 'group2'], + }; + const permissions1 = { + read: principals1, + write: principals1, + }; + const acl1 = new ACL(permissions1); + const result1 = acl1 + .removePermission(['read'], { + users: ['user1'], + groups: [], + }) + .removePermission(['write'], { + users: [], + groups: ['group2'], + }) + .getPermissions(); + expect(result1?.read?.users).toEqual([]); + expect(result1?.write?.groups).toEqual(['group1']); + + const principals2: Principals = { + users: ['*'], + groups: ['*'], + }; + + const permissions2 = { + read: principals2, + write: principals2, + }; + + const acl2 = new ACL(permissions2); + const result2 = acl2 + .removePermission(['read', 'write'], { + users: ['user1'], + groups: ['group1'], + }) + .getPermissions(); + expect(result2?.read?.users).toEqual(['*']); + expect(result2?.write?.groups).toEqual(['*']); + }); + + it('test transform permission', () => { + const principals: Principals = { + users: ['user1'], + groups: ['group1', 'group2'], + }; + const permissions = { + read: principals, + write: principals, + }; + const acl = new ACL(permissions); + const result = acl.toFlatList(); + expect(result).toHaveLength(3); + expect(result).toEqual( + expect.arrayContaining([{ type: 'users', name: 'user1', permissions: ['read', 'write'] }]) + ); + expect(result).toEqual( + expect.arrayContaining([{ type: 'groups', name: 'group1', permissions: ['read', 'write'] }]) + ); + expect(result).toEqual( + expect.arrayContaining([{ type: 'groups', name: 'group2', permissions: ['read', 'write'] }]) + ); + }); + + it('test generate query DSL', () => { + const principals = { + users: ['user1'], + groups: ['group1'], + }; + const result = ACL.generateGetPermittedSavedObjectsQueryDSL(['read'], principals, 'workspace'); + expect(result).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + terms: { + 'permissions.read.users': ['user1'], + }, + }, + { + term: { + 'permissions.read.users': '*', + }, + }, + { + terms: { + 'permissions.read.groups': ['group1'], + }, + }, + { + term: { + 'permissions.read.groups': '*', + }, + }, + ], + }, + }, + { + terms: { + type: ['workspace'], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/src/core/server/saved_objects/permission_control/acl.ts b/src/core/server/saved_objects/permission_control/acl.ts new file mode 100644 index 000000000000..1631b0cbef46 --- /dev/null +++ b/src/core/server/saved_objects/permission_control/acl.ts @@ -0,0 +1,249 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum PrincipalType { + Users = 'users', + Groups = 'groups', +} + +export interface Principals { + users?: string[]; + groups?: string[]; +} + +export type Permissions = Record; + +export interface TransformedPermission { + type: string; + name: string; + permissions: string[]; +} + +const addToPrincipals = (principals?: Principals, users?: string[], groups?: string[]) => { + if (!principals) { + principals = {}; + } + if (!!users) { + if (!principals.users) { + principals.users = []; + } + principals.users = Array.from(new Set([...principals.users, ...users])); + } + if (!!groups) { + if (!principals.groups) { + principals.groups = []; + } + principals.groups = Array.from(new Set([...principals.groups, ...groups])); + } + return principals; +}; + +const deleteFromPrincipals = (principals?: Principals, users?: string[], groups?: string[]) => { + if (!principals) { + return principals; + } + if (!!users && !!principals.users) { + principals.users = principals.users.filter((item) => !users.includes(item)); + } + if (!!groups && !!principals.groups) { + principals.groups = principals.groups.filter((item) => !groups.includes(item)); + } + return principals; +}; + +const checkPermission = (currentPrincipals: Principals | undefined, principals: Principals) => { + return ( + (currentPrincipals?.users && + principals?.users && + checkPermissionForSinglePrincipalType(currentPrincipals.users, principals.users)) || + (currentPrincipals?.groups && + principals.groups && + checkPermissionForSinglePrincipalType(currentPrincipals.groups, principals.groups)) + ); +}; + +const checkPermissionForSinglePrincipalType = ( + currentPrincipalArray: string[], + principalArray: string[] +) => { + return ( + currentPrincipalArray && + principalArray && + (currentPrincipalArray.includes('*') || + principalArray.some((item) => currentPrincipalArray.includes(item))) + ); +}; + +export class ACL { + private permissions?: Permissions; + constructor(initialPermissions?: Permissions) { + this.permissions = initialPermissions || {}; + } + + // parse the permissions object to check whether the specific principal has the specific permission types or not + public hasPermission(permissionTypes: string[], principals: Principals) { + if (!permissionTypes || permissionTypes.length === 0 || !this.permissions || !principals) { + return false; + } + + const currentPermissions = this.permissions; + return permissionTypes.some((permissionType) => + checkPermission(currentPermissions[permissionType], principals) + ); + } + + // permissions object build function, add principal with specific permission to the object + public addPermission(permissionTypes: string[], principals: Principals) { + if (!permissionTypes || !principals) { + return this; + } + if (!this.permissions) { + this.permissions = {}; + } + + for (const permissionType of permissionTypes) { + this.permissions[permissionType] = addToPrincipals( + this.permissions[permissionType], + principals.users, + principals.groups + ); + } + + return this; + } + + // permissions object build function, remove specific permission of specific principal from the object + public removePermission(permissionTypes: string[], principals: Principals) { + if (!permissionTypes || !principals) { + return this; + } + if (!this.permissions) { + this.permissions = {}; + } + + for (const permissionType of permissionTypes) { + const result = deleteFromPrincipals( + this.permissions![permissionType], + principals.users, + principals.groups + ); + if (result) { + this.permissions[permissionType] = result; + } + } + + return this; + } + + /** + * transform permissions format + * original permissions: { + * read: { + * users:['user1'] + * }, + * write:{ + * groups:['group1'] + * } + * } + * + * transformed permissions: [ + * {type:'users',name:'user1',permissions:['read']}, + * {type:'groups',name:'group1',permissions:['write']}, + * ] + */ + public toFlatList(): TransformedPermission[] { + const result: TransformedPermission[] = []; + if (!this.permissions) { + return result; + } + + for (const permissionType in this.permissions) { + if (Object.prototype.hasOwnProperty.call(this.permissions, permissionType)) { + const { users = [], groups = [] } = this.permissions[permissionType] ?? {}; + users.forEach((user) => { + const found = result.find((r) => r.type === PrincipalType.Users && r.name === user); + if (found) { + found.permissions.push(permissionType); + } else { + result.push({ type: PrincipalType.Users, name: user, permissions: [permissionType] }); + } + }); + groups.forEach((group) => { + const found = result.find((r) => r.type === PrincipalType.Groups && r.name === group); + if (found) { + found.permissions.push(permissionType); + } else { + result.push({ type: PrincipalType.Groups, name: group, permissions: [permissionType] }); + } + }); + } + } + + return result; + } + + public resetPermissions() { + // reset permissions + this.permissions = {}; + } + + // return the permissions object + public getPermissions() { + return this.permissions; + } + + /** + * generate query DSL by the specific conditions, used for fetching saved objects from the saved objects index + */ + public static generateGetPermittedSavedObjectsQueryDSL( + permissionTypes: string[], + principals: Principals, + savedObjectType?: string | string[] + ) { + if (!principals || !permissionTypes) { + return { + query: { + match_none: {}, + }, + }; + } + + const bool: any = { + filter: [], + }; + const subBool: any = { + should: [], + }; + + permissionTypes.forEach((permissionType) => { + Object.entries(principals).forEach(([principalType, principalsInCurrentType]) => { + subBool.should.push({ + terms: { + ['permissions.' + permissionType + `.${principalType}`]: principalsInCurrentType, + }, + }); + subBool.should.push({ + term: { + ['permissions.' + permissionType + `.${principalType}`]: '*', + }, + }); + }); + }); + + bool.filter.push({ + bool: subBool, + }); + + if (!!savedObjectType) { + bool.filter.push({ + terms: { + type: Array.isArray(savedObjectType) ? savedObjectType : [savedObjectType], + }, + }); + } + + return { query: { bool } }; + } +} From d7d712e350922e00a9efbe309bbed02f4463374c Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 19 Sep 2023 17:21:04 +0800 Subject: [PATCH 3/4] feat: add core workspace module (#145) The core workspace module(WorkspaceService) is a foundational component that enables the implementation of workspace features within OSD plugins. The purpose of the core workspace module is to provide a framework for workspace implementations. This module does not implement specific workspace functionality but provides the essential infrastructure for plugins to extend and customize workspace features, it maintains a shared workspace state(observables) across the entire application to ensure a consistent and up-to-date view of workspace-related information to all parts of the application. --------- Signed-off-by: Yulong Ruan --- src/core/public/application/types.ts | 3 + src/core/public/core_system.ts | 8 + src/core/public/index.ts | 13 + src/core/public/mocks.ts | 4 + src/core/public/plugins/plugin_context.ts | 2 + .../public/plugins/plugins_service.test.ts | 3 + src/core/public/workspace/index.ts | 10 + .../workspace/workspaces_service.mock.ts | 36 +++ .../public/workspace/workspaces_service.ts | 134 ++++++++ src/core/types/workspace.ts | 1 + .../dashboard_listing.test.tsx.snap | 240 +++++++++++++++ .../dashboard_top_nav.test.tsx.snap | 288 ++++++++++++++++++ 12 files changed, 742 insertions(+) create mode 100644 src/core/public/workspace/index.ts create mode 100644 src/core/public/workspace/workspaces_service.mock.ts create mode 100644 src/core/public/workspace/workspaces_service.ts diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 7398aad65009..4744ab34cfd3 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -47,6 +47,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; +import { WorkspacesStart } from '../workspace'; /** * Accessibility status of an application. @@ -334,6 +335,8 @@ export interface AppMountContext { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; + /** {@link WorkspacesService} */ + workspaces: WorkspacesStart; }; } diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 8fe5a36ebb55..d7de8b4595d5 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -54,6 +54,7 @@ import { ContextService } from './context'; import { IntegrationsService } from './integrations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; +import { WorkspacesService } from './workspace'; interface Params { rootDomElement: HTMLElement; @@ -110,6 +111,7 @@ export class CoreSystem { private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; + private readonly workspaces: WorkspacesService; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -138,6 +140,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); + this.workspaces = new WorkspacesService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; @@ -160,6 +163,7 @@ 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 = this.workspaces.setup(); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ @@ -176,6 +180,7 @@ export class CoreSystem { injectedMetadata, notifications, uiSettings, + workspaces, }; // Services that do not expose contracts at setup @@ -220,6 +225,7 @@ export class CoreSystem { targetDomElement: notificationsTargetDomElement, }); const application = await this.application.start({ http, overlays }); + const workspaces = this.workspaces.start({ application, http }); const chrome = await this.chrome.start({ application, docLinks, @@ -242,6 +248,7 @@ export class CoreSystem { overlays, savedObjects, uiSettings, + workspaces, })); const core: InternalCoreStart = { @@ -256,6 +263,7 @@ export class CoreSystem { overlays, uiSettings, fatalErrors, + workspaces, }; await this.plugins.start(core); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 03ef6b6392f9..14ab91e1cb13 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -87,6 +87,7 @@ import { HandlerParameters, } from './context'; import { Branding } from '../types'; +import { WorkspacesStart, WorkspacesSetup } from './workspace'; export type { Logos } from '../common'; export { PackageInfo, EnvironmentMode } from '../server/types'; @@ -102,6 +103,7 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, + WorkspaceAttribute, } from '../types'; export { @@ -239,6 +241,8 @@ export interface CoreSetup; + /** {@link WorkspacesSetup} */ + workspaces: WorkspacesSetup; } /** @@ -293,6 +297,8 @@ export interface CoreStart { getInjectedVar: (name: string, defaultValue?: any) => unknown; getBranding: () => Branding; }; + /** {@link WorkspacesStart} */ + workspaces: WorkspacesStart; } export { @@ -341,3 +347,10 @@ export { }; export { __osdBootstrap__ } from './osd_bootstrap'; + +export { + WorkspacesStart, + WorkspacesSetup, + WorkspacesService, + WorkspaceObservables, +} from './workspace'; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e863d627c801..722070d5a9ea 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -47,6 +47,7 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; +import { workspacesServiceMock } from './workspace/workspaces_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -60,6 +61,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; +export { workspacesServiceMock } from './workspace/workspaces_service.mock'; function createCoreSetupMock({ basePath = '', @@ -85,6 +87,7 @@ function createCoreSetupMock({ getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, getBranding: injectedMetadataServiceMock.createSetupContract().getBranding, }, + workspaces: workspacesServiceMock.createSetupContractMock(), }; return mock; @@ -106,6 +109,7 @@ function createCoreStartMock({ basePath = '' } = {}) { getBranding: injectedMetadataServiceMock.createStartContract().getBranding, }, fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 42c40e91183f..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, }; } @@ -168,5 +169,6 @@ export function createPluginStartContext< getBranding: deps.injectedMetadata.getBranding, }, fatalErrors: deps.fatalErrors, + workspaces: deps.workspaces, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index b2cf4e8880cf..7e8c96c1f9d0 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -58,6 +58,7 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; +import { workspacesServiceMock } from '../workspace/workspaces_service.mock'; export let mockPluginInitializers: Map; @@ -108,6 +109,7 @@ describe('PluginsService', () => { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + workspaces: workspacesServiceMock.createSetupContractMock(), }; mockSetupContext = { ...mockSetupDeps, @@ -127,6 +129,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + workspaces: workspacesServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts new file mode 100644 index 000000000000..4ef6aaae7fd4 --- /dev/null +++ b/src/core/public/workspace/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { + WorkspacesStart, + WorkspacesService, + WorkspacesSetup, + WorkspaceObservables, +} from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts new file mode 100644 index 000000000000..3c35315aa850 --- /dev/null +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { WorkspaceAttribute } from '..'; + +const currentWorkspaceId$ = new BehaviorSubject(''); +const workspaceList$ = new BehaviorSubject([]); +const currentWorkspace$ = new BehaviorSubject(null); +const initialized$ = new BehaviorSubject(false); +const workspaceEnabled$ = new BehaviorSubject(false); + +const createWorkspacesSetupContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, + workspaceEnabled$, + registerWorkspaceMenuRender: jest.fn(), +}); + +const createWorkspacesStartContractMock = () => ({ + currentWorkspaceId$, + workspaceList$, + currentWorkspace$, + initialized$, + workspaceEnabled$, + renderWorkspaceMenu: jest.fn(), +}); + +export const workspacesServiceMock = { + createSetupContractMock: createWorkspacesSetupContractMock, + createStartContract: createWorkspacesStartContractMock, +}; diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts new file mode 100644 index 000000000000..39519ccdddbe --- /dev/null +++ b/src/core/public/workspace/workspaces_service.ts @@ -0,0 +1,134 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { isEqual } from 'lodash'; + +import { CoreService, WorkspaceAttribute } from '../../types'; +import { InternalApplicationStart } from '../application'; +import { HttpSetup } from '../http'; + +type WorkspaceMenuRenderFn = ({ + basePath, + getUrlForApp, + observables, +}: { + getUrlForApp: InternalApplicationStart['getUrlForApp']; + basePath: HttpSetup['basePath']; + observables: WorkspaceObservables; +}) => JSX.Element | null; + +type WorkspaceObject = WorkspaceAttribute & { readonly?: boolean }; + +export interface WorkspaceObservables { + currentWorkspaceId$: BehaviorSubject; + currentWorkspace$: BehaviorSubject; + workspaceList$: BehaviorSubject; + workspaceEnabled$: BehaviorSubject; + initialized$: BehaviorSubject; +} + +enum WORKSPACE_ERROR { + WORKSPACE_STALED = 'WORKSPACE_STALED', +} + +/** + * @public + */ +export interface WorkspacesSetup extends WorkspaceObservables { + registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => void; +} + +export interface WorkspacesStart extends WorkspaceObservables { + renderWorkspaceMenu: () => JSX.Element | null; +} + +export class WorkspacesService implements CoreService { + private currentWorkspaceId$ = new BehaviorSubject(''); + private workspaceList$ = new BehaviorSubject([]); + private currentWorkspace$ = new BehaviorSubject(null); + private initialized$ = new BehaviorSubject(false); + private workspaceEnabled$ = new BehaviorSubject(false); + private _renderWorkspaceMenu: WorkspaceMenuRenderFn | null = null; + + constructor() { + combineLatest([this.initialized$, this.workspaceList$, this.currentWorkspaceId$]).subscribe( + ([workspaceInitialized, workspaceList, currentWorkspaceId]) => { + if (workspaceInitialized) { + const currentWorkspace = workspaceList.find((w) => w && w.id === currentWorkspaceId); + + /** + * Do a simple idempotent verification here + */ + if (!isEqual(currentWorkspace, this.currentWorkspace$.getValue())) { + this.currentWorkspace$.next(currentWorkspace ?? null); + } + + if (currentWorkspaceId && !currentWorkspace?.id) { + /** + * Current workspace is staled + */ + this.currentWorkspaceId$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_STALED, + }); + this.currentWorkspace$.error({ + reason: WORKSPACE_ERROR.WORKSPACE_STALED, + }); + } + } + } + ); + } + + public setup(): WorkspacesSetup { + return { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + workspaceEnabled$: this.workspaceEnabled$, + registerWorkspaceMenuRender: (render: WorkspaceMenuRenderFn) => + (this._renderWorkspaceMenu = render), + }; + } + + public start({ + http, + application, + }: { + application: InternalApplicationStart; + http: HttpSetup; + }): WorkspacesStart { + const observables = { + currentWorkspaceId$: this.currentWorkspaceId$, + currentWorkspace$: this.currentWorkspace$, + workspaceList$: this.workspaceList$, + initialized$: this.initialized$, + workspaceEnabled$: this.workspaceEnabled$, + }; + return { + ...observables, + renderWorkspaceMenu: () => { + if (this._renderWorkspaceMenu) { + return this._renderWorkspaceMenu({ + basePath: http.basePath, + getUrlForApp: application.getUrlForApp, + observables, + }); + } + return null; + }, + }; + } + + public async stop() { + this.currentWorkspace$.unsubscribe(); + this.currentWorkspaceId$.unsubscribe(); + this.workspaceList$.unsubscribe(); + this.workspaceEnabled$.unsubscribe(); + this.initialized$.unsubscribe(); + this._renderWorkspaceMenu = null; + } +} diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts index 23c3b2038ff2..e99744183cac 100644 --- a/src/core/types/workspace.ts +++ b/src/core/types/workspace.ts @@ -11,4 +11,5 @@ export interface WorkspaceAttribute { color?: string; icon?: string; defaultVISTheme?: string; + reserved?: boolean; } diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index 1b3c486615c8..d3d0fe94fb68 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -993,6 +993,54 @@ exports[`dashboard listing hideWriteControls 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -2085,6 +2133,54 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -3238,6 +3334,54 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -4391,6 +4535,54 @@ exports[`dashboard listing renders table rows 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -5544,6 +5736,54 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 101e0e520304..0c48ffdc474a 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -859,6 +859,54 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -1776,6 +1824,54 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -2693,6 +2789,54 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -3610,6 +3754,54 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -4527,6 +4719,54 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > @@ -5444,6 +5684,54 @@ exports[`Dashboard top nav render with all components 1`] = ` "allowTrackUserAgent": [MockFunction], "reportUiStats": [MockFunction], }, + "workspaces": Object { + "currentWorkspace$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "currentWorkspaceId$": BehaviorSubject { + "_isScalar": false, + "_value": "", + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "initialized$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "renderWorkspaceMenu": [MockFunction], + "workspaceEnabled$": BehaviorSubject { + "_isScalar": false, + "_value": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "workspaceList$": BehaviorSubject { + "_isScalar": false, + "_value": Array [], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, } } > From f630277ee6b5822cc5caa9469f75802bcda15e47 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Wed, 20 Sep 2023 17:37:45 +0800 Subject: [PATCH 4/4] use self hosted runner and disable windows workflow (#182) disable github workflows running on windows for development (#161) --------- Signed-off-by: Yulong Ruan Signed-off-by: SuZhou-Joe Co-authored-by: SuZhou-Joe --- .github/workflows/build_and_test_workflow.yml | 13 ++----------- .github/workflows/cypress_workflow.yml | 8 ++++---- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index a9175ad23f8b..540d771e84bd 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -33,13 +33,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [arc-runner-set] group: [1, 2, 3, 4] include: - os: ubuntu-latest name: Linux - - os: windows-latest - name: Windows runs-on: ${{ matrix.os }} steps: - name: Configure git's autocrlf (Windows only) @@ -130,13 +128,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [arc-runner-set] group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] include: - os: ubuntu-latest name: Linux - - os: windows-latest - name: Windows runs-on: ${{ matrix.os }} steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} @@ -227,11 +223,6 @@ jobs: ext: tar.gz suffix: linux-arm64 script: build-platform --linux-arm --skip-os-packages - - os: windows-latest - name: Windows x64 - ext: zip - suffix: windows-x64 - script: build-platform --windows --skip-os-packages runs-on: ${{ matrix.os }} defaults: run: diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 5e78785f9b88..cb5af78fcfc5 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -19,7 +19,7 @@ env: jobs: cypress-tests: - runs-on: ubuntu-latest + runs-on: arc-runner-set container: image: docker://opensearchstaging/ci-runner:ci-runner-rockylinux8-opensearch-dashboards-integtest-v2 options: --user 1001 @@ -54,7 +54,7 @@ jobs: uses: actions/checkout@v2 with: path: ${{ env.FTR_PATH }} - repository: opensearch-project/opensearch-dashboards-functional-test + repository: ruanyl/opensearch-dashboards-functional-test ref: '${{ github.base_ref }}' - name: Get Cypress version @@ -88,7 +88,7 @@ jobs: name: ftr-cypress-screenshots path: ${{ env.FTR_PATH }}/cypress/screenshots retention-days: 1 - + - uses: actions/upload-artifact@v3 if: always() with: @@ -101,4 +101,4 @@ jobs: with: name: ftr-cypress-results path: ${{ env.FTR_PATH }}/cypress/results - retention-days: 1 \ No newline at end of file + retention-days: 1