From c83393b53ef937e622bb3452d6c7c25f33b0f7f5 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 22 Aug 2023 17:22:51 +0800 Subject: [PATCH] supports configure workspace features with wildcard (#96) supports configure workspace features with wildcard --------- Signed-off-by: Yulong Ruan --- src/core/public/index.ts | 2 +- src/core/public/workspace/index.ts | 1 - .../workspace/workspaces_service.mock.ts | 2 +- .../public/workspace/workspaces_service.ts | 12 +-- src/core/server/index.ts | 2 +- src/core/types/index.ts | 1 + src/core/types/workspace.ts | 14 ++++ src/plugins/workspace/public/plugin.ts | 3 +- src/plugins/workspace/public/utils.test.ts | 83 +++++++++++++++++++ src/plugins/workspace/public/utils.ts | 52 +++++++++++- src/plugins/workspace/server/plugin.ts | 5 +- src/plugins/workspace/server/types.ts | 11 +-- .../workspace/server/workspace_client.ts | 8 +- 13 files changed, 166 insertions(+), 30 deletions(-) create mode 100644 src/core/types/workspace.ts create mode 100644 src/plugins/workspace/public/utils.test.ts diff --git a/src/core/public/index.ts b/src/core/public/index.ts index d31febbd40d1..a3995734857c 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -103,6 +103,7 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, + WorkspaceAttribute, } from '../types'; export { @@ -352,7 +353,6 @@ export { WorkspaceStart, WorkspaceSetup, WorkspaceService, - WorkspaceAttribute, WorkspaceObservables, } from './workspace'; diff --git a/src/core/public/workspace/index.ts b/src/core/public/workspace/index.ts index c446ecda4499..d83bb2c90909 100644 --- a/src/core/public/workspace/index.ts +++ b/src/core/public/workspace/index.ts @@ -8,4 +8,3 @@ export { WorkspaceSetup, WorkspaceObservables, } from './workspaces_service'; -export type { WorkspaceAttribute } from './workspaces_service'; diff --git a/src/core/public/workspace/workspaces_service.mock.ts b/src/core/public/workspace/workspaces_service.mock.ts index b45d38542b55..d9190c28176f 100644 --- a/src/core/public/workspace/workspaces_service.mock.ts +++ b/src/core/public/workspace/workspaces_service.mock.ts @@ -4,7 +4,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { WorkspaceAttribute } from '../workspace'; +import { WorkspaceAttribute } from '..'; const currentWorkspaceId$ = new BehaviorSubject(''); const workspaceList$ = new BehaviorSubject([]); diff --git a/src/core/public/workspace/workspaces_service.ts b/src/core/public/workspace/workspaces_service.ts index d11a4f5da380..9a1504808551 100644 --- a/src/core/public/workspace/workspaces_service.ts +++ b/src/core/public/workspace/workspaces_service.ts @@ -4,7 +4,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { CoreService } from '../../types'; +import { CoreService, WorkspaceAttribute } from '../../types'; import { InternalApplicationStart } from '../application'; import { HttpSetup } from '../http'; @@ -36,16 +36,6 @@ export interface WorkspaceStart extends WorkspaceObservables { renderWorkspaceMenu: () => JSX.Element | null; } -export interface WorkspaceAttribute { - id: string; - name: string; - description?: string; - features?: string[]; - color?: string; - icon?: string; - defaultVISTheme?: string; -} - export class WorkspaceService implements CoreService { private currentWorkspaceId$ = new BehaviorSubject(''); private workspaceList$ = new BehaviorSubject([]); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d150c4af2117..93e784e7a4ca 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -353,7 +353,7 @@ export { MetricsServiceStart, } from './metrics'; -export { AppCategory } from '../types'; +export { AppCategory, WorkspaceAttribute } from '../types'; export { DEFAULT_APP_CATEGORIES, WorkspacePermissionMode, 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/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 1c6c59248016..db380017a5c5 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -32,6 +32,7 @@ import { WorkspaceClient } from './workspace_client'; import { IndexPatternManagementSetup } from '../../index_pattern_management/public'; import { renderWorkspaceMenu } from './render_workspace_menu'; import { Services } from './types'; +import { featureMatchesConfig } from './utils'; interface WorkspacePluginSetupDeps { savedObjectsManagement?: SavedObjectsManagementPluginSetup; @@ -163,7 +164,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> private filterByWorkspace(workspace: WorkspaceAttribute | null, allNavLinks: ChromeNavLink[]) { if (!workspace) return allNavLinks; const features = workspace.features ?? []; - return allNavLinks.filter((item) => features.includes(item.id)); + return allNavLinks.filter(featureMatchesConfig(features)); } private filterNavLinks(core: CoreStart) { diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts new file mode 100644 index 000000000000..a24fcdce5ee4 --- /dev/null +++ b/src/plugins/workspace/public/utils.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { featureMatchesConfig } from './utils'; + +describe('workspace utils: featureMatchesConfig', () => { + it('feature configured with `*` should match any features', () => { + const match = featureMatchesConfig(['*']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + expect( + match({ id: 'discover', category: { id: 'opensearchDashboards', label: 'Library' } }) + ).toBe(true); + }); + + it('should NOT match the config if feature id not matches', () => { + const match = featureMatchesConfig(['discover', 'dashboards', 'visualize']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + false + ); + }); + + it('should match the config if feature id matches', () => { + const match = featureMatchesConfig(['discover', 'dashboards', 'visualize']); + expect( + match({ id: 'discover', category: { id: 'opensearchDashboards', label: 'Library' } }) + ).toBe(true); + }); + + it('should match the config if feature category matches', () => { + const match = featureMatchesConfig(['discover', 'dashboards', '@management', 'visualize']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + }); + + it('should match any features but not the excluded feature id', () => { + const match = featureMatchesConfig(['*', '!discover']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + expect( + match({ id: 'discover', category: { id: 'opensearchDashboards', label: 'Library' } }) + ).toBe(false); + }); + + it('should match any features but not the excluded feature category', () => { + const match = featureMatchesConfig(['*', '!@management']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + false + ); + expect(match({ id: 'integrations', category: { id: 'management', label: 'Management' } })).toBe( + false + ); + expect( + match({ id: 'discover', category: { id: 'opensearchDashboards', label: 'Library' } }) + ).toBe(true); + }); + + it('should match features of a category but NOT the excluded feature', () => { + const match = featureMatchesConfig(['@management', '!dev_tools']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + false + ); + expect(match({ id: 'integrations', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + }); + + it('a config presents later in the config array should override the previous config', () => { + // though `dev_tools` is excluded, but this config will override by '@management' as dev_tools has category 'management' + const match = featureMatchesConfig(['!dev_tools', '@management']); + expect(match({ id: 'dev_tools', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + expect(match({ id: 'integrations', category: { id: 'management', label: 'Management' } })).toBe( + true + ); + }); +}); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index ccb286860061..09528c2b080f 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -4,7 +4,7 @@ */ import { WORKSPACE_PATH_PREFIX } from '../../../core/public/utils'; -import { IBasePath } from '../../../core/public'; +import { AppCategory, IBasePath } from '../../../core/public'; export const formatUrlWithWorkspaceId = ( url: string, @@ -29,3 +29,53 @@ export const formatUrlWithWorkspaceId = ( return newUrl.toString(); }; + +/** + * Given a list of feature config, check if a feature matches config + * Rules: + * 1. `*` matches any feature + * 2. config starts with `@` matches category, for example, @management matches any feature of `management` category + * 3. to match a specific feature, just use the feature id, such as `discover` + * 4. to exclude feature or category, use `!@management` or `!discover` + * 5. the order of featureConfig array matters, from left to right, the later config override the previous config, + * for example, ['!@management', '*'] matches any feature because '*' overrides the previous setting: '!@management' + */ +export const featureMatchesConfig = (featureConfigs: string[]) => ({ + id, + category, +}: { + id: string; + category?: AppCategory; +}) => { + let matched = false; + + for (const featureConfig of featureConfigs) { + // '*' matches any feature + if (featureConfig === '*') { + matched = true; + } + + // The config starts with `@` matches a category + if (category && featureConfig === `@${category.id}`) { + matched = true; + } + + // The config matches a feature id + if (featureConfig === id) { + matched = true; + } + + // If a config starts with `!`, such feature or category will be excluded + if (featureConfig.startsWith('!')) { + if (category && featureConfig === `!@${category.id}`) { + matched = false; + } + + if (featureConfig === `!${id}`) { + matched = false; + } + } + } + + return matched; +}; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 1d3cdf3ad19d..ff5bf3a09933 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -19,8 +19,10 @@ import { Permissions, WorkspacePermissionMode, SavedObjectsClient, + WorkspaceAttribute, + DEFAULT_APP_CATEGORIES, } from '../../../core/server'; -import { IWorkspaceDBImpl, WorkspaceAttribute } from './types'; +import { IWorkspaceDBImpl } from './types'; import { WorkspaceClientWithSavedObject } from './workspace_client'; import { WorkspaceSavedObjectsClientWrapper } from './saved_objects'; import { registerRoutes } from './routes'; @@ -147,6 +149,7 @@ export class WorkspacePlugin implements Plugin<{}, {}> { name: i18n.translate('workspaces.management.workspace.default.name', { defaultMessage: 'Management', }), + features: [`@${DEFAULT_APP_CATEGORIES.management.id}`], }, managementWorkspaceACL.getPermissions() ), diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index 72afd87fff90..3d0be3ac824d 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -10,18 +10,9 @@ import { CoreSetup, WorkspacePermissionMode, Permissions, + WorkspaceAttribute, } from '../../../core/server'; -export interface WorkspaceAttribute { - id: string; - name: string; - description?: string; - features?: string[]; - color?: string; - icon?: string; - defaultVISTheme?: string; -} - export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { permissions: Permissions; } diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index a44060e7193b..01a75d416e76 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -2,11 +2,15 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import type { SavedObject, SavedObjectsClientContract, CoreSetup } from '../../../core/server'; +import type { + SavedObject, + SavedObjectsClientContract, + CoreSetup, + WorkspaceAttribute, +} from '../../../core/server'; import { WORKSPACE_TYPE } from '../../../core/server'; import { IWorkspaceDBImpl, - WorkspaceAttribute, WorkspaceFindOptions, IResponse, IRequestDetail,