From 0a6312cbc0c74f5d666e080572cb3be7a8231d67 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Sat, 24 Aug 2024 11:50:47 +0800 Subject: [PATCH 01/21] user level setting Signed-off-by: Hailong Cui --- src/core/server/index.ts | 1 + .../create_or_upgrade_saved_config.ts | 9 +- .../get_upgradeable_config.ts | 4 + src/core/server/ui_settings/index.ts | 2 +- .../server/ui_settings/ui_settings_client.ts | 67 +++- src/core/types/ui_settings.ts | 6 + src/core/types/workspace.ts | 7 + .../opensearch_dashboards.json | 2 +- .../management_app/advanced_settings.tsx | 1 + .../mount_management_section.tsx | 26 +- .../public/management_app/user_settings.tsx | 105 ++++++ .../advanced_settings/public/plugin.ts | 61 +++- src/plugins/advanced_settings/public/types.ts | 6 + .../server/capabilities_provider.ts | 3 + .../advanced_settings/server/plugin.ts | 45 ++- .../user_ui_settings_client_wrapper.ts | 158 ++++++++ .../user_ui_settings_client_wrapper.ts.bak | 189 ++++++++++ src/plugins/advanced_settings/server/utils.ts | 21 ++ src/plugins/content_management/public/app.tsx | 12 +- .../public/components/page_render.tsx | 26 +- .../content_management/public/plugin.ts | 4 +- .../content_management/public/types.ts | 13 +- src/plugins/workspace/common/constants.ts | 4 + src/plugins/workspace/common/types.ts | 6 + .../components/workspace_form/constants.ts | 7 +- .../public/components/workspace_form/utils.ts | 3 +- .../workspace_permission_setting_input.tsx | 7 +- .../workspace_permission_setting_panel.tsx | 7 +- .../workspace_list/default_workspace.tsx | 54 +++ .../components/workspace_list/index.tsx | 340 +++++++++++------- src/plugins/workspace/public/plugin.ts | 32 ++ src/plugins/workspace/public/types.ts | 2 +- src/plugins/workspace/server/plugin.ts | 4 + src/plugins/workspace/server/routes/index.ts | 15 +- src/plugins/workspace/server/types.ts | 5 +- src/plugins/workspace/server/ui_settings.ts | 19 + src/plugins/workspace/server/utils.ts | 51 ++- 37 files changed, 1159 insertions(+), 165 deletions(-) create mode 100644 src/plugins/advanced_settings/public/management_app/user_settings.tsx create mode 100644 src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts create mode 100644 src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak create mode 100644 src/plugins/advanced_settings/server/utils.ts create mode 100644 src/plugins/workspace/public/components/workspace_list/default_workspace.tsx create mode 100644 src/plugins/workspace/server/ui_settings.ts diff --git a/src/core/server/index.ts b/src/core/server/index.ts index de78a2160618..14d7b93c5f68 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -358,6 +358,7 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, + CURRENT_USER, } from './ui_settings'; export { diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 639dd09249ff..bf64fa909829 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -35,6 +35,7 @@ import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; +import { CURRENT_USER } from '../ui_settings_client'; interface Options { savedObjectsClient: SavedObjectsClientContract; @@ -42,17 +43,19 @@ interface Options { buildNum: number; log: Logger; handleWriteErrors: boolean; + userLevel?: boolean; } export async function createOrUpgradeSavedConfig( options: Options ): Promise | undefined> { - const { savedObjectsClient, version, buildNum, log, handleWriteErrors } = options; + const { savedObjectsClient, version, buildNum, log, handleWriteErrors, userLevel } = options; // try to find an older config we can upgrade const upgradeableConfig = await getUpgradeableConfig({ savedObjectsClient, version, + userLevel, }); // default to the attributes of the upgradeableConfig if available @@ -62,8 +65,9 @@ export async function createOrUpgradeSavedConfig( ); try { + const docId = userLevel ? `${CURRENT_USER}_${version}` : version; // create the new SavedConfig - await savedObjectsClient.create('config', attributes, { id: version }); + await savedObjectsClient.create('config', attributes, { id: docId }); } catch (error) { if (handleWriteErrors) { if (SavedObjectsErrorHelpers.isConflictError(error)) { @@ -85,6 +89,7 @@ export async function createOrUpgradeSavedConfig( log.debug(`Upgrade config from ${upgradeableConfig.id} to ${version}`, { prevVersion: upgradeableConfig.id, newVersion: version, + userLevel: !!userLevel, }); } } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 9e1100a91896..416233c8ff4e 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -29,6 +29,7 @@ */ import { SavedObjectsClientContract } from '../../saved_objects/types'; +import { CURRENT_USER } from '../ui_settings_client'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; /** @@ -41,9 +42,11 @@ import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; export async function getUpgradeableConfig({ savedObjectsClient, version, + userLevel, }: { savedObjectsClient: SavedObjectsClientContract; version: string; + userLevel?: boolean; }) { // attempt to find a config we can upgrade const { saved_objects: savedConfigs } = await savedObjectsClient.find({ @@ -52,6 +55,7 @@ export async function getUpgradeableConfig({ perPage: 1000, sortField: 'buildNum', sortOrder: 'desc', + ...(userLevel ? { hasReference: { type: 'user', id: CURRENT_USER } } : {}), }); // try to find a config that we can upgrade diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index 7912c0af84af..a3cfa84f2ac7 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -28,7 +28,7 @@ * under the License. */ -export { UiSettingsClient, UiSettingsServiceOptions } from './ui_settings_client'; +export { UiSettingsClient, UiSettingsServiceOptions, CURRENT_USER } from './ui_settings_client'; export { config } from './ui_settings_config'; export { UiSettingsService } from './ui_settings_service'; diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index e78f79746546..aeef5c843930 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -36,6 +36,7 @@ import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; +import { HttpServiceStart } from '..'; export interface UiSettingsServiceOptions { type: string; @@ -45,11 +46,13 @@ export interface UiSettingsServiceOptions { overrides?: Record; defaults?: Record; log: Logger; + httpStart?: HttpServiceStart; } interface ReadOptions { ignore401Errors?: boolean; autoCreateOrUpgradeIfMissing?: boolean; + userLevel?: boolean; } interface UserProvidedValue { @@ -62,6 +65,9 @@ type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; type UserProvided = Record>; type UiSettingsRaw = Record; +// identifier for current user +export const CURRENT_USER = ''; + export class UiSettingsClient implements IUiSettingsClient { private readonly type: UiSettingsServiceOptions['type']; private readonly id: UiSettingsServiceOptions['id']; @@ -116,21 +122,36 @@ export class UiSettingsClient implements IUiSettingsClient { } async getUserProvided(): Promise> { + // user provided for global const userProvided: UserProvided = this.onReadHook(await this.read()); + // personal level settings + const personalLevelProvided: UserProvided = this.onReadHook( + await this.read({ userLevel: true }) + ); // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object for (const [key, value] of Object.entries(this.overrides)) { userProvided[key] = value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; + + personalLevelProvided[key] = + value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } - return userProvided; + return { ...userProvided, ...personalLevelProvided }; } async setMany(changes: Record) { this.onWriteHook(changes); - await this.write({ changes }); + // group changes into by different scope + const [global, personal] = this.groupChanges(changes); + if (global && Object.keys(global).length > 0) { + await this.write({ changes: global }); + } + if (personal && Object.keys(personal).length > 0) { + await this.write({ changes: personal, userLevel: true }); + } } async set(key: string, value: any) { @@ -201,16 +222,45 @@ export class UiSettingsClient implements IUiSettingsClient { return filteredValues; } + /** + * group change into different scopes + * @param changes ui setting changes + * @returns [global, user] + */ + private groupChanges(changes: Record) { + const userLevelKeys = [] as string[]; + Object.entries(this.defaults).forEach(([key, value]) => { + if (value.scope === 'user' || (Array.isArray(value.scope) && value.scope.includes('user'))) { + userLevelKeys.push(key); + } + }); + const userChanges = {} as Record; + const globalChanges = {} as Record; + + Object.entries(changes).forEach(([key, val]) => { + if (userLevelKeys.includes(key)) { + userChanges[key] = val; + } else { + globalChanges[key] = val; + } + }); + + return [globalChanges, userChanges]; + } + private async write({ changes, autoCreateOrUpgradeIfMissing = true, + userLevel = false, }: { changes: Record; autoCreateOrUpgradeIfMissing?: boolean; + userLevel?: boolean; }) { changes = this.translateChanges(changes, 'timeline', 'timelion'); try { - await this.savedObjectsClient.update(this.type, this.id, changes); + const docId = userLevel ? `${CURRENT_USER}_${this.id}` : this.id; + await this.savedObjectsClient.update(this.type, docId, changes); } catch (error) { if (!SavedObjectsErrorHelpers.isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) { throw error; @@ -222,11 +272,13 @@ export class UiSettingsClient implements IUiSettingsClient { buildNum: this.buildNum, log: this.log, handleWriteErrors: false, + userLevel, }); await this.write({ changes, autoCreateOrUpgradeIfMissing: false, + userLevel, }); } } @@ -234,9 +286,14 @@ export class UiSettingsClient implements IUiSettingsClient { private async read({ ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, + userLevel = false, }: ReadOptions = {}): Promise> { + let docId = this.id; + if (userLevel) { + docId = `${CURRENT_USER}_${this.id}`; + } try { - const resp = await this.savedObjectsClient.get>(this.type, this.id); + const resp = await this.savedObjectsClient.get>(this.type, docId); return this.translateChanges(resp.attributes, 'timelion', 'timeline'); } catch (error) { if (SavedObjectsErrorHelpers.isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { @@ -246,12 +303,14 @@ export class UiSettingsClient implements IUiSettingsClient { buildNum: this.buildNum, log: this.log, handleWriteErrors: true, + userLevel, }); if (!failedUpgradeAttributes) { return await this.read({ ignore401Errors, autoCreateOrUpgradeIfMissing: false, + userLevel, }); } diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index b3ad0e54fa04..e8c6b9130b9e 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -56,6 +56,8 @@ export interface DeprecationSettings { docLinksKey: string; } +export type SettingScope = 'global' | 'user'; + /** * UiSettings parameters defined by the plugins. * @public @@ -63,6 +65,10 @@ export interface DeprecationSettings { export interface UiSettingsParams { /** title in the UI */ name?: string; + /** + * scope of the setting item + */ + scope?: SettingScope | SettingScope[]; /** default value to fall back to if a user doesn't provide any */ value?: T; /** description provided to a user in UI */ diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts index c00d3576d567..aa7a3073f377 100644 --- a/src/core/types/workspace.ts +++ b/src/core/types/workspace.ts @@ -5,6 +5,12 @@ import { Permissions } from '../server/saved_objects'; +export enum PermissionModeId { + Read = 'read', + ReadAndWrite = 'read+write', + Owner = 'owner', +} + export interface WorkspaceAttribute { id: string; name: string; @@ -19,4 +25,5 @@ export interface WorkspaceAttribute { export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { permissions?: Permissions; + permissionMode?: PermissionModeId; } diff --git a/src/plugins/advanced_settings/opensearch_dashboards.json b/src/plugins/advanced_settings/opensearch_dashboards.json index 97848475efca..30d5ac045c74 100644 --- a/src/plugins/advanced_settings/opensearch_dashboards.json +++ b/src/plugins/advanced_settings/opensearch_dashboards.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["management","navigation"], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "contentManagement"], "requiredBundles": ["opensearchDashboardsReact", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index f5034ce08d2b..a7bcaabc323b 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -172,6 +172,7 @@ export class AdvancedSettingsComponent extends Component< const all = config.getAll(); const userSettingsEnabled = config.get('theme:enableUserControl'); return Object.entries(all) + .filter((setting) => setting[1].scope !== 'user') .map((setting) => { return toEditableConfig({ def: setting[1], diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index 608324d0ecb2..c5f3db562a59 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -34,15 +34,18 @@ import { Router, Switch, Route } from 'react-router-dom'; import { i18n } from '@osd/i18n'; import { I18nProvider } from '@osd/i18n/react'; -import { StartServicesAccessor } from 'src/core/public'; +import { AppMountParameters, CoreStart, StartServicesAccessor } from 'src/core/public'; import { EuiPageContent } from '@elastic/eui'; +import { ContentManagementPluginStart } from '../../../content_management/public'; import { AdvancedSettings } from './advanced_settings'; import { ManagementAppMountParams } from '../../../management/public'; import { ComponentRegistry } from '../types'; -import { NavigationPublicPluginStart } from '../../../../plugins/navigation/public'; +import { NavigationPublicPluginStart } from '../../../navigation/public'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; import './index.scss'; +import { UserSettingsApp } from './user_settings'; const readOnlyBadge = { text: i18n.translate('advancedSettings.badge.readOnly.text', { @@ -131,3 +134,22 @@ export async function mountManagementSection( ReactDOM.unmountComponentAtNode(params.element); }; } + +export const renderUserSettingsApp = async ( + { element }: AppMountParameters, + services: CoreStart & { + contentManagement: ContentManagementPluginStart; + navigation: NavigationPublicPluginStart; + } +) => { + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/advanced_settings/public/management_app/user_settings.tsx b/src/plugins/advanced_settings/public/management_app/user_settings.tsx new file mode 100644 index 000000000000..d0a1424fd585 --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/user_settings.tsx @@ -0,0 +1,105 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiBreadcrumb, EuiPage, EuiPanel } from '@elastic/eui'; +import { CoreStart } from 'opensearch-dashboards/public'; +import React, { useEffect } from 'react'; +import { i18n } from '@osd/i18n'; +import { + ContentManagementPluginSetup, + ContentManagementPluginStart, + Content, +} from '../../../content_management/public'; +import { NavigationPublicPluginStart } from '../../../navigation/public'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; + +const sectionRender = (contents: Content[]) => { + return ( + <> + {contents.map((content) => { + if (content.kind === 'custom') { + return {content.render()}; + } + + return null; + })} + + ); +}; + +export const setupUserSettingsPage = (contentManagement?: ContentManagementPluginSetup) => { + contentManagement?.registerPage({ + id: 'user_settings', + title: 'Home', + sections: [ + { + id: 'user_profile', + order: 1000, + title: `User's profile`, + kind: 'custom', + render: sectionRender, + }, + { + id: 'default_workspace', + order: 2000, + kind: 'custom', + render: sectionRender, + }, + { + id: 'user_identity_role', + order: 3000, + kind: 'custom', + render: sectionRender, + }, + ], + }); +}; + +export const UserSettingsApp = () => { + const { + services: { + contentManagement, + application, + chrome, + navigation: { + ui: { HeaderControl }, + }, + }, + } = useOpenSearchDashboards< + CoreStart & { + contentManagement: ContentManagementPluginStart; + navigation: NavigationPublicPluginStart; + } + >(); + + useEffect(() => { + const breadcrumbs: EuiBreadcrumb[] = [ + { + text: i18n.translate('advancedSettings.userSettingsLabel', { + defaultMessage: 'User settings', + }), + }, + ]; + chrome.setBreadcrumbs(breadcrumbs); + }, [chrome]); + + return ( + + + {contentManagement + ? contentManagement.renderPage('user_settings', { fragmentOnly: true }) + : null} + + ); +}; diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 8c840150529e..d3143d0e2397 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -29,7 +29,14 @@ */ import { i18n } from '@osd/i18n'; -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; +import { + AppMountParameters, + AppUpdater, + CoreSetup, + CoreStart, + Plugin, +} from 'opensearch-dashboards/public'; +import { BehaviorSubject } from 'rxjs'; import { FeatureCatalogueCategory } from '../../home/public'; import { ComponentRegistry } from './component_registry'; import { @@ -41,6 +48,7 @@ import { import { setupTopNavThemeButton } from './register_nav_control'; import { DEFAULT_NAV_GROUPS, AppNavLinkStatus, WorkspaceAvailability } from '../../../core/public'; import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; +import { setupUserSettingsPage } from './management_app/user_settings'; const component = new ComponentRegistry(); @@ -60,9 +68,11 @@ export class AdvancedSettingsPlugin AdvancedSettingsPluginSetup, AdvancedSettingsPluginStart > { + private appUpdater$ = new BehaviorSubject(() => undefined); + public setup( core: CoreSetup, - { management, home }: AdvancedSettingsPluginSetup + { management, home, contentManagement: contentManagementSetup }: AdvancedSettingsPluginSetup ) { const opensearchDashboardsSection = management.sections.section.opensearchDashboards; @@ -116,6 +126,44 @@ export class AdvancedSettingsPlugin }, ]); + if (core.chrome.navGroup.getNavGroupEnabled()) { + setupUserSettingsPage(contentManagementSetup); + + core.application.registerAppUpdater(this.appUpdater$); + + const userSettingTitle = i18n.translate('advancedSettings.userSettingsLabel', { + defaultMessage: 'User settings', + }); + + core.application.register({ + id: 'user_settings', + title: userSettingTitle, + updater$: this.appUpdater$, + navLinkStatus: core.chrome.navGroup.getNavGroupEnabled() + ? AppNavLinkStatus.visible + : AppNavLinkStatus.hidden, + workspaceAvailability: WorkspaceAvailability.outsideWorkspace, + description: i18n.translate('advancedSettings.userSettings.description', { + defaultMessage: 'Configure your personal preferences.', + }), + mount: async (params: AppMountParameters) => { + const { renderUserSettingsApp } = await import( + './management_app/mount_management_section' + ); + const [coreStart, { contentManagement, navigation }] = await core.getStartServices(); + + return renderUserSettingsApp(params, { ...coreStart, contentManagement, navigation }); + }, + }); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.settingsAndSetup, [ + { + id: 'user_settings', + order: 101, + }, + ]); + } + if (home) { home.featureCatalogue.register({ id: 'advanced_settings', @@ -142,6 +190,15 @@ export class AdvancedSettingsPlugin setupTopNavThemeButton(core, core.uiSettings.get('home:useNewHomePage')); } + this.appUpdater$.next((app) => { + const securityEnabled = core.application.capabilities.useSettings?.enabled; + if (app.id === 'user_settings') { + return { + navLinkStatus: securityEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + }; + } + }); + return { component: component.start, }; diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index 68d92a2b856f..e7c349d1135a 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -28,6 +28,10 @@ * under the License. */ +import { + ContentManagementPluginStart, + ContentManagementPluginSetup, +} from '../../content_management/public'; import { ComponentRegistry } from './component_registry'; import { HomePublicPluginSetup } from '../../home/public'; @@ -44,10 +48,12 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + contentManagement?: ContentManagementPluginSetup; } export interface AdvancedSettingsPluginStart { navigation: NavigationPublicPluginStart; + contentManagement: ContentManagementPluginStart; } export { ComponentRegistry }; diff --git a/src/plugins/advanced_settings/server/capabilities_provider.ts b/src/plugins/advanced_settings/server/capabilities_provider.ts index be87d687d4b9..9b78577a0948 100644 --- a/src/plugins/advanced_settings/server/capabilities_provider.ts +++ b/src/plugins/advanced_settings/server/capabilities_provider.ts @@ -33,4 +33,7 @@ export const capabilitiesProvider = () => ({ show: true, save: true, }, + useSettings: { + enabled: false, + }, }); diff --git a/src/plugins/advanced_settings/server/plugin.ts b/src/plugins/advanced_settings/server/plugin.ts index 094d975acf91..bd9993d0ecd2 100644 --- a/src/plugins/advanced_settings/server/plugin.ts +++ b/src/plugins/advanced_settings/server/plugin.ts @@ -34,34 +34,73 @@ import { CoreStart, Plugin, Logger, + SharedGlobalConfig, } from 'opensearch-dashboards/server'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { capabilitiesProvider } from './capabilities_provider'; +import { UserUISettingsClientWrapper } from './saved_objects/user_ui_settings_client_wrapper'; +import { extractUserName } from './utils'; export class AdvancedSettingsServerPlugin implements Plugin { private readonly logger: Logger; + private userUiSettingsClientWrapper?: UserUISettingsClientWrapper; + private coreStart: CoreStart | undefined; + private readonly globalConfig$: Observable; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); + this.globalConfig$ = initializerContext.config.legacy.globalConfig$; } - public setup(core: CoreSetup) { + public async setup(core: CoreSetup) { this.logger.debug('advancedSettings: Setup'); core.capabilities.registerProvider(capabilitiesProvider); - core.capabilities.registerSwitcher(async (request, capabilites) => { - return await core.security.readonlyService().hideForReadonly(request, capabilites, { + core.capabilities.registerSwitcher(async (request, capabilities) => { + return await core.security.readonlyService().hideForReadonly(request, capabilities, { advancedSettings: { save: false, }, }); }); + const globalConfig = await this.globalConfig$.pipe(first()).toPromise(); + const isPermissionControlEnabled = globalConfig.savedObjects.permission.enabled === true; + + const userUiSettingsClientWrapper = new UserUISettingsClientWrapper( + this.logger, + isPermissionControlEnabled + ); + this.userUiSettingsClientWrapper = userUiSettingsClientWrapper; + core.savedObjects.addClientWrapper( + 3, + 'user_ui_settings', + userUiSettingsClientWrapper.wrapperFactory + ); + + core.capabilities.registerSwitcher(async (request, capabilities) => { + const userName = extractUserName(request, this.coreStart); + if (userName) { + return { + ...capabilities, + useSettings: { + enabled: true, + }, + }; + } + return capabilities; + }); + return {}; } public start(core: CoreStart) { this.logger.debug('advancedSettings: Started'); + this.coreStart = core; + this.userUiSettingsClientWrapper?.setCore(core); + return {}; } diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts new file mode 100644 index 000000000000..5db43f92d819 --- /dev/null +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -0,0 +1,158 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SavedObject, + SavedObjectsBaseOptions, + SavedObjectsClientWrapperFactory, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, + CoreStart, + ACL, + SavedObjectsCreateOptions, + SavedObjectsFindOptions, + SavedObjectsFindResponse, + OpenSearchDashboardsRequest, +} from '../../../../core/server'; +import { Logger, CURRENT_USER } from '../../../../core/server'; +import { extractUserName } from '../utils'; + +/** + * This saved object client wrapper offers methods to get and update UI settings considering + * the context of the current user + */ +export class UserUISettingsClientWrapper { + constructor( + private readonly logger: Logger, + private readonly savedObjectsPermissionEnabled: boolean + ) {} + private core?: CoreStart; + + public setCore(core: CoreStart) { + this.core = core; + } + + private isUserLevelSetting(id: string | undefined): boolean { + return id ? id.startsWith(CURRENT_USER) : false; + } + + private normalizeDocId(id: string, request: OpenSearchDashboardsRequest, core?: CoreStart) { + const userName = extractUserName(request, core); + if (userName && this.isUserLevelSetting(id)) { + return id.replace(CURRENT_USER, userName); + } + return id; + } + + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { + const getUiSettings = async ( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> => { + if (type === 'config') { + const docId = this.normalizeDocId(id, wrapperOptions.request, this.core); + this.logger.debug(`Getting config with original: ${id} normalizeDocId: ${docId}`); + // user level + return wrapperOptions.client.get(type, docId, options); + } + + return wrapperOptions.client.get(type, id, options); + }; + + const updateUiSettings = async ( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> => { + if (type === 'config') { + const docId = this.normalizeDocId(id, wrapperOptions.request, this.core); + this.logger.debug(`Getting config with original: ${id} normalizeDocId: ${docId}`); + // update user level settings + return await wrapperOptions.client.update(type, docId, attributes, options); + } + return wrapperOptions.client.update(type, id, attributes, options); + }; + + const createUiSettings = async ( + type: string, + attributes: T, + options?: SavedObjectsCreateOptions + ) => { + const userName = extractUserName(wrapperOptions.request, this.core); + const { id } = options || {}; + const userLevel = this.isUserLevelSetting(id); + + if (type === 'config' && userLevel && userName && id) { + const permissions = { + permissions: new ACL() + .addPermission(['write'], { + users: [userName], + }) + .getPermissions()!, + }; + + const docId = this.normalizeDocId(id, wrapperOptions.request, this.core); + // create with reference, the reference field will used for filter settings by user + return await wrapperOptions.client.create(type, attributes, { + ...options, + id: docId, + references: [ + { + type: 'user', // dummy type + id: userName, + name: userName, + }, + ], + ...(this.savedObjectsPermissionEnabled ? permissions : {}), + }); + } + return wrapperOptions.client.create(type, attributes, options); + }; + + const findUiSettings = async ( + options: SavedObjectsFindOptions + ): Promise> => { + // check if options type is config + const userName = extractUserName(wrapperOptions.request, this.core); + const { hasReference } = options || {}; + if (options.type === 'config' && userName && hasReference) { + const id = hasReference.id.replace(CURRENT_USER, userName); + const resp: SavedObjectsFindResponse = await wrapperOptions.client.find({ + ...options, + hasReference: { ...hasReference, id }, + }); + + // normalize the document id to real version + resp.saved_objects.forEach((so) => { + so.id = so.id.replace(`${userName}_`, ''); + }); + + return new Promise((resolve) => { + resolve(resp); + }); + } + return wrapperOptions.client.find(options); + }; + + return { + ...wrapperOptions.client, + checkConflicts: wrapperOptions.client.checkConflicts, + errors: wrapperOptions.client.errors, + addToNamespaces: wrapperOptions.client.addToNamespaces, + deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, + find: findUiSettings, + bulkGet: wrapperOptions.client.bulkGet, + create: createUiSettings, + bulkCreate: wrapperOptions.client.bulkCreate, + delete: wrapperOptions.client.delete, + bulkUpdate: wrapperOptions.client.bulkUpdate, + deleteByWorkspace: wrapperOptions.client.deleteByWorkspace, + get: getUiSettings, + update: updateUiSettings, + }; + }; +} diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak new file mode 100644 index 000000000000..6aafae1987e8 --- /dev/null +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak @@ -0,0 +1,189 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SavedObject, + SavedObjectsBaseOptions, + SavedObjectsClientWrapperFactory, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, + CoreStart, + AuthStatus, + SavedObjectsErrorHelpers, + OpenSearchDashboardsRequest, + ACL, + SavedObjectsCreateOptions, +} from '../../../../core/server'; +import { Logger } from '../../../../core/server'; + +export interface AuthInfo { + backend_roles?: string[]; + user_name?: string; +} + +export const hashUserName = (username: string) => { + return username; +}; + +/** + * This saved object client wrapper offers methods to get and update UI settings considering + * the context of the current user + */ +export class UserUISettingsClientWrapper { + constructor( + private readonly logger: Logger, + private readonly savedObjectsPermissionEnabled: boolean + ) {} + private core: CoreStart | undefined; + + public setCore(core: CoreStart) { + this.core = core; + } + + private extractUserName(request: OpenSearchDashboardsRequest) { + const authInfoResp = this.core?.http.auth.get(request); + + if (authInfoResp?.status === AuthStatus.authenticated) { + const authInfo = authInfoResp?.state as { authInfo: AuthInfo } | null; + return authInfo?.authInfo?.user_name; + } + return undefined; + } + + public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { + const getUiSettings = async ( + type: string, + id: string, + options: SavedObjectsBaseOptions = {} + ): Promise> => { + const userName = this.extractUserName(wrapperOptions.request); + + /** + * When getting ui settings within a workspace, it will combine the workspace ui settings with + * the global ui settings and workspace ui settings have higher priority if the same setting + * was defined in both places + */ + if (type === 'config' && userName) { + this.logger.debug(`getting ui settings for ${userName} with id ${id}`); + // global value + const configObject = await wrapperOptions.client.get>( + 'config', + id, + options + ); + try { + // user level + const userConfigObject = await wrapperOptions.client.get>( + 'config', + hashUserName(userName), + options + ); + + configObject.attributes = { + ...configObject.attributes, + ...(userConfigObject ? userConfigObject.attributes : {}), + }; + } catch (err) { + // ignore + } + + return configObject as SavedObject; + } + + return wrapperOptions.client.get(type, id, options); + }; + + const updateUiSettings = async ( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> => { + const userName = this.extractUserName(wrapperOptions.request); + + if (type === 'config' && userName && this.core) { + const allRegisteredSettings = this.core.uiSettings + .asScopedToClient(wrapperOptions.client) + .getRegistered(); + + const userLevelKeys = [] as string[]; + Object.keys(allRegisteredSettings).forEach((key) => { + const setting = allRegisteredSettings[key]; + if (setting.scope === 'user') { + userLevelKeys.push(key); + } + }); + + const userLevelSettings = {} as Partial; + const globalSettings = {} as Partial; + + Object.entries(attributes).forEach(([key, value]) => { + if (userLevelKeys.includes(key)) { + Object.assign(userLevelSettings, { [key]: value }); + } else { + Object.assign(globalSettings, { [key]: value }); + } + }); + + let updateRes: SavedObjectsUpdateResponse = { + attributes, + references: undefined, + id, + type: 'config', + }; + + const docId = hashUserName(userName); + + // update user level settings + if (Object.keys(userLevelSettings).length > 0) { + try { + await wrapperOptions.client.update(type, docId, userLevelSettings, options); + } catch (error) { + if (!SavedObjectsErrorHelpers.isNotFoundError(error)) { + throw error; + } + + let createOptions: SavedObjectsCreateOptions = { id: docId }; + if (this.savedObjectsPermissionEnabled) { + const permissionObject = new ACL() + .addPermission(['write'], { + users: [userName], + }) + .getPermissions(); + + createOptions = { ...options, permissions: permissionObject }; + } + + await wrapperOptions.client.create(type, userLevelSettings, createOptions); + } + } + + // update global ui settings + if (Object.keys(globalSettings).length > 0) { + updateRes = await wrapperOptions.client.update(type, id, globalSettings, options); + } + return updateRes; + } + return wrapperOptions.client.update(type, id, attributes, options); + }; + + return { + ...wrapperOptions.client, + checkConflicts: wrapperOptions.client.checkConflicts, + errors: wrapperOptions.client.errors, + addToNamespaces: wrapperOptions.client.addToNamespaces, + deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, + find: wrapperOptions.client.find, + bulkGet: wrapperOptions.client.bulkGet, + create: wrapperOptions.client.create, + bulkCreate: wrapperOptions.client.bulkCreate, + delete: wrapperOptions.client.delete, + bulkUpdate: wrapperOptions.client.bulkUpdate, + deleteByWorkspace: wrapperOptions.client.deleteByWorkspace, + get: getUiSettings, + update: updateUiSettings, + }; + }; +} diff --git a/src/plugins/advanced_settings/server/utils.ts b/src/plugins/advanced_settings/server/utils.ts new file mode 100644 index 000000000000..f50e3c981a78 --- /dev/null +++ b/src/plugins/advanced_settings/server/utils.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthStatus, CoreStart, OpenSearchDashboardsRequest } from '../../../core/server'; + +export interface AuthInfo { + backend_roles?: string[]; + user_name?: string; +} + +export const extractUserName = (request: OpenSearchDashboardsRequest, core?: CoreStart) => { + const authInfoResp = core?.http.auth.get(request); + + if (authInfoResp?.status === AuthStatus.authenticated) { + const authInfo = authInfoResp?.state as { authInfo: AuthInfo } | null; + return authInfo?.authInfo?.user_name; + } + return undefined; +}; diff --git a/src/plugins/content_management/public/app.tsx b/src/plugins/content_management/public/app.tsx index 794d36d7e946..0abe07f707e0 100644 --- a/src/plugins/content_management/public/app.tsx +++ b/src/plugins/content_management/public/app.tsx @@ -9,15 +9,25 @@ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { PageRender } from './components/page_render'; import { Page } from './services'; import { EmbeddableStart } from '../../embeddable/public'; +import { RenderOptions } from './types'; export const renderPage = ({ page, embeddable, savedObjectsClient, + renderOptions, }: { page: Page; embeddable: EmbeddableStart; savedObjectsClient: SavedObjectsClientContract; + renderOptions?: RenderOptions; }) => { - return ; + return ( + + ); }; diff --git a/src/plugins/content_management/public/components/page_render.tsx b/src/plugins/content_management/public/components/page_render.tsx index f0cdca8bd900..101624aa3289 100644 --- a/src/plugins/content_management/public/components/page_render.tsx +++ b/src/plugins/content_management/public/components/page_render.tsx @@ -11,19 +11,27 @@ import { EuiFlexItem, EuiPage, EuiSpacer } from '@elastic/eui'; import { Page } from '../services'; import { SectionRender } from './section_render'; import { EmbeddableStart } from '../../../embeddable/public'; +import { RenderOptions } from '../types'; export interface Props { page: Page; embeddable: EmbeddableStart; savedObjectsClient: SavedObjectsClientContract; + renderOptions?: RenderOptions; } -export const PageRender = ({ page, embeddable, savedObjectsClient }: Props) => { +export const PageRender = ({ page, embeddable, savedObjectsClient, renderOptions }: Props) => { const sections = useObservable(page.getSections$()) || []; - return ( - - {sections.map((section, i) => ( + let finalRenderSections = sections; + const { sectionId, fragmentOnly } = renderOptions || {}; + if (sectionId) { + finalRenderSections = sections.filter((section) => section.id === sectionId); + } + + const sectionRenderResult = ( + <> + {finalRenderSections.map((section, i) => ( { {i < sections.length - 1 && } ))} + + ); + + if (fragmentOnly) { + return sectionRenderResult; + } + + return ( + + {sectionRenderResult} ); }; diff --git a/src/plugins/content_management/public/plugin.ts b/src/plugins/content_management/public/plugin.ts index 6b5feeaeb9a1..91c3b1a979ff 100644 --- a/src/plugins/content_management/public/plugin.ts +++ b/src/plugins/content_management/public/plugin.ts @@ -11,6 +11,7 @@ import { ContentManagementPluginSetupDependencies, ContentManagementPluginStart, ContentManagementPluginStartDependencies, + RenderOptions, } from './types'; import { CUSTOM_CONTENT_EMBEDDABLE } from './components/custom_content_embeddable'; import { CustomContentEmbeddableFactoryDefinition } from './components/custom_content_embeddable_factory'; @@ -65,13 +66,14 @@ export class ContentManagementPublicPlugin return { registerContentProvider: this.contentManagementService.registerContentProvider, updatePageSection: this.contentManagementService.updatePageSection, - renderPage: (id: string) => { + renderPage: (id: string, renderOptions?: RenderOptions) => { const page = this.contentManagementService.getPage(id); if (page) { return renderPage({ page, embeddable: depsStart.embeddable, savedObjectsClient: core.savedObjects.client, + renderOptions, }); } }, diff --git a/src/plugins/content_management/public/types.ts b/src/plugins/content_management/public/types.ts index a2c1f506b9c0..e925408a7823 100644 --- a/src/plugins/content_management/public/types.ts +++ b/src/plugins/content_management/public/types.ts @@ -12,6 +12,17 @@ import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; export interface ContentManagementPluginSetup { registerPage: ContentManagementService['registerPage']; } +export interface RenderOptions { + /** + * show as fragment not a full page + */ + fragmentOnly?: boolean; + /** + * only render specific section when specified + */ + sectionId?: string; +} + export interface ContentManagementPluginStart { /** * @experimental this API is experimental and might change in future releases @@ -25,7 +36,7 @@ export interface ContentManagementPluginStart { targetArea: string, callback: (section: Section | null, err?: Error) => Section | null ) => void; - renderPage: (id: string) => React.ReactNode; + renderPage: (id: string, options?: RenderOptions) => React.ReactNode; } export interface ContentManagementPluginStartDependencies { diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index f66f7ea71752..fac1c57f4d07 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -16,6 +16,10 @@ export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; export const WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace_conflict_control'; export const WORKSPACE_UI_SETTINGS_CLIENT_WRAPPER_ID = 'workspace_ui_settings'; +/** + * UI setting for user default workspace + */ +export const DEFAULT_WORKSPACE = 'defaultWorkspace'; export enum WorkspacePermissionMode { Read = 'read', diff --git a/src/plugins/workspace/common/types.ts b/src/plugins/workspace/common/types.ts index 4b64f62d9e29..06f5c602bda6 100644 --- a/src/plugins/workspace/common/types.ts +++ b/src/plugins/workspace/common/types.ts @@ -27,3 +27,9 @@ export interface DataSourceConnection { description?: string; relatedConnections?: DataSourceConnection[]; } + +export enum PermissionModeId { + Read = 'read', + ReadAndWrite = 'read+write', + Owner = 'owner', +} diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index ed2268bec8d7..03179f84a6f6 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -5,6 +5,7 @@ import { i18n } from '@osd/i18n'; import { WorkspacePermissionMode } from '../../../common/constants'; +import { PermissionModeId } from '../../../common/types'; export enum WorkspaceOperationType { Create = 'create', @@ -16,12 +17,6 @@ export enum WorkspacePermissionItemType { Group = 'group', } -export enum PermissionModeId { - Read = 'read', - ReadAndWrite = 'read+write', - Owner = 'owner', -} - export const optionIdToWorkspacePermissionModesMap: { [key: string]: WorkspacePermissionMode[]; } = { diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 161def1f3f6b..476ddf25fbbc 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -10,7 +10,6 @@ import { CURRENT_USER_PLACEHOLDER, WorkspacePermissionMode } from '../../../comm import { isUseCaseFeatureConfig } from '../../utils'; import { optionIdToWorkspacePermissionModesMap, - PermissionModeId, WorkspaceOperationType, WorkspacePermissionItemType, } from './constants'; @@ -25,7 +24,7 @@ import { WorkspaceUserGroupPermissionSetting, WorkspaceUserPermissionSetting, } from './types'; -import { DataSource } from '../../../common/types'; +import { DataSource, PermissionModeId } from '../../../common/types'; import { validateWorkspaceColor } from '../../../common/utils'; export const isValidFormTextInput = (input?: string) => { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx index 00560f7c033d..60b043932283 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx @@ -14,12 +14,9 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { - WorkspacePermissionItemType, - optionIdToWorkspacePermissionModesMap, - PermissionModeId, -} from './constants'; +import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; import { getPermissionModeId } from './utils'; +import { PermissionModeId } from '../../../common/types'; const permissionModeOptions: Array> = [ { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx index 8ea255b83b36..854d90b6de2b 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx @@ -13,16 +13,13 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { WorkspaceFormError, WorkspacePermissionSetting } from './types'; -import { - WorkspacePermissionItemType, - optionIdToWorkspacePermissionModesMap, - PermissionModeId, -} from './constants'; +import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; import { WorkspacePermissionSettingInput, WorkspacePermissionSettingInputProps, } from './workspace_permission_setting_input'; import { generateNextPermissionSettingsId } from './utils'; +import { PermissionModeId } from '../../../common/types'; export interface WorkspacePermissionSettingPanelProps { errors?: { [key: number]: WorkspaceFormError }; diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx new file mode 100644 index 000000000000..8008d47dc691 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { BehaviorSubject, of } from 'rxjs'; +import { useObservable } from 'react-use'; +import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dashboards_react/public'; +import { WorkspaceListInner } from '.'; +import { Services, WorkspaceUseCase } from '../../types'; + +interface Props { + services: Services; + registeredUseCases$: BehaviorSubject; +} + +export const DefaultWorkspaceList = ({ services, registeredUseCases$ }: Props) => { + const { workspaces } = services; + const workspaceList = useObservable(workspaces?.workspaceList$ ?? of([]), []); + + return ( + + + + + +

Workspaces ({workspaceList?.length})

+
+
+ + + + + + +
+
+
+ ); +}; diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 88e80609a410..ceaafbd15bb9 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -16,14 +16,19 @@ import { EuiSearchBarProps, copyToClipboard, EuiTableSelectionType, - EuiButtonEmpty, EuiButton, EuiEmptyPrompt, EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTableProps, + EuiTableProps, } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, of } from 'rxjs'; import { i18n } from '@osd/i18n'; +import { isString } from 'lodash'; import { DEFAULT_NAV_GROUPS, WorkspaceAttribute, @@ -33,7 +38,7 @@ import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashb import { navigateToWorkspaceDetail } from '../utils/workspace'; import { DetailTab } from '../workspace_form/constants'; -import { WORKSPACE_CREATE_APP_ID } from '../../../common/constants'; +import { DEFAULT_WORKSPACE, WORKSPACE_CREATE_APP_ID } from '../../../common/constants'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; import { getFirstUseCaseOfFeatureConfigs, getDataSourcesList } from '../../utils'; @@ -41,17 +46,45 @@ import { WorkspaceUseCase } from '../../types'; import { NavigationPublicPluginStart } from '../../../../../plugins/navigation/public'; import { WorkspacePermissionMode } from '../../../common/constants'; import { DataSourceAttributesWithWorkspaces } from '../../types'; +import { PermissionModeId } from '../../../common/types'; export interface WorkspaceListProps { registeredUseCases$: BehaviorSubject; } +export interface WorkspaceListInnerProps extends WorkspaceListProps { + fullPage?: boolean; + selectable?: boolean; + searchable?: boolean; + excludedActionNames?: string[]; + includedColumns?: Array<{ field: string; width?: string }>; + excludedColumns?: string[]; +} + interface WorkspaceAttributeWithUseCaseIDAndDataSources extends WorkspaceAttribute { useCase?: string; dataSources?: string[]; } -export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { +export const WorkspaceList = (props: WorkspaceListProps) => { + return ( + + ); +}; + +export const WorkspaceListInner = ({ + registeredUseCases$, + fullPage = true, + selectable = true, + searchable = true, + excludedActionNames = [], + includedColumns = [], + excludedColumns = [], +}: WorkspaceListInnerProps) => { const { services: { workspaces, @@ -60,6 +93,7 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { navigationUI: { HeaderControl }, uiSettings, savedObjects, + notifications, }, } = useOpenSearchDashboards<{ navigationUI: NavigationPublicPluginStart['ui']; @@ -72,12 +106,14 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 5, + pageSize: 10, pageSizeOptions: [5, 10, 20], }); const [deletedWorkspaces, setDeletedWorkspaces] = useState([]); const [selection, setSelection] = useState([]); const [allDataSources, setAllDataSources] = useState([]); + // default workspace state + const [defaultWorkspace, setDefaultWorkspace] = useState(undefined); const dateFormat = uiSettings?.get('dateFormat'); @@ -99,12 +135,13 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { ); useEffect(() => { + setDefaultWorkspace(uiSettings?.get(DEFAULT_WORKSPACE)); if (savedObjects) { getDataSourcesList(savedObjects.client, ['*']).then((data) => { setAllDataSources(data); }); } - }, [savedObjects]); + }, [savedObjects, uiSettings]); const newWorkspaceList: WorkspaceAttributeWithUseCaseIDAndDataSources[] = useMemo(() => { return workspaceList.map( @@ -188,6 +225,7 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { const handleCopyId = (id: string) => { copyToClipboard(id); + notifications?.toasts.addSuccess('Workspace ID copied'); }; const handleSwitchWorkspace = useCallback( @@ -199,6 +237,25 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { [application, http] ); + const handleSetDefaultWorkspace = useCallback( + async (item: WorkspaceAttribute) => { + const set = await uiSettings?.set(DEFAULT_WORKSPACE, item.id); + if (set) { + setDefaultWorkspace(item.id); + notifications?.toasts.addSuccess(`Default workspace been set to ${item.name}`); + } else { + // toast + notifications?.toasts.addError( + new Error(`Failed to set workspace ${item.name} as default workspace.`), + { + title: 'Set default workspace error', + } + ); + } + }, + [notifications?.toasts, uiSettings] + ); + const renderDataWithMoreBadge = ( data: string[], maxDisplayedAmount: number, @@ -295,16 +352,25 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { toolsLeft: renderToolsLeft(), }; - const columns = [ + const columnsWithoutActions = [ { field: 'name', name: 'Name', - width: '15%', + width: '18%', sortable: true, render: (name: string, item: WorkspaceAttributeWithPermission) => ( handleSwitchWorkspace(item.id)}> - {name} + + + {name} + + {item.id === defaultWorkspace && ( + + Default workspace + + )} + ), @@ -313,7 +379,7 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { { field: 'useCase', name: 'Use case', - width: '15%', + width: '12%', }, { @@ -326,7 +392,7 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { content={description} data-test-subj="workspaceList-hover-description" > - {/* Here I need to set width mannuly as the tooltip will ineffect the property : truncateText ', */} + {/* Here I need to set width manually as the tooltip will ineffect the property : truncateText ', */} {description} @@ -345,6 +411,11 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { return renderDataWithMoreBadge(owners, 1, item.id, DetailTab.Collaborators); }, }, + { + field: 'permissionMode', + name: 'Permissions', + width: '6%', + }, { field: 'lastUpdatedTime', name: 'Last updated', @@ -362,124 +433,153 @@ export const WorkspaceList = ({ registeredUseCases$ }: WorkspaceListProps) => { return renderDataWithMoreBadge(dataSources, 2, item.id, DetailTab.DataSources); }, }, + ]; + const allActions = [ + { + name: Copy ID, + icon: 'copy', + type: 'icon', + description: 'Copy workspace id', + 'data-test-subj': 'workspace-list-copy-id-icon', + onClick: ({ id }: WorkspaceAttribute) => handleCopyId(id), + }, + { + name: Edit, + icon: 'pencil', + type: 'icon', + description: 'Edit workspace', + 'data-test-subj': 'workspace-list-edit-icon', + onClick: ({ id }: WorkspaceAttribute) => handleSwitchWorkspace(id), + }, + { + name: Set as default, + icon: 'flag', + type: 'icon', + description: 'Set as default workspace', + 'data-test-subj': 'workspace-list-set-default-icon', + onClick: (item: WorkspaceAttribute) => handleSetDefaultWorkspace(item), + }, + { + name: Leave, + icon: 'exit', + type: 'icon', + description: 'Leave workspace', + 'data-test-subj': 'workspace-list-leave-icon', + enabled: (item: WorkspaceAttributeWithPermission) => + item.permissionMode === PermissionModeId.Owner, + available: () => false, + }, + { + name: ( + + Delete + + ), + icon: () => , + type: 'icon', + isPrimary: false, + description: 'Delete workspace', + 'data-test-subj': 'workspace-list-delete-icon', + onClick: (item: WorkspaceAttribute) => { + setDeletedWorkspaces([item]); + }, + }, + ]; + + const availableActions = allActions.filter( + (action) => + !excludedActionNames?.includes(isString(action.name) ? action.name : action.name.key || '') + ); + + const includedColumnsFields = includedColumns.map((column) => { + return column.field; + }); + const availableColumns = columnsWithoutActions + .filter((column) => !excludedColumns.includes(column.field)) + .filter((column) => { + return ( + !includedColumnsFields || + includedColumnsFields.length === 0 || + includedColumnsFields.includes(column.field) + ); + }) + .map((column) => { + const customizedCol = includedColumns.find((col) => col.field === column.field); + return { + ...column, + ...(customizedCol ? { ...customizedCol } : {}), + }; + }); + + const actionColumns = [ { name: 'Actions', field: '', - actions: [ - { - name: 'Copy ID', - type: 'button', - description: 'Copy id', - 'data-test-subj': 'workspace-list-copy-id-icon', - render: ({ id }: WorkspaceAttribute) => { - return ( - handleCopyId(id)} - size="xs" - iconType="copy" - color="text" - > - Copy ID - - ); - }, - }, - { - name: 'Edit', - type: 'icon', - icon: 'edit', - color: 'danger', - description: 'Edit workspace', - 'data-test-subj': 'workspace-list-edit-icon', - onClick: ({ id }: WorkspaceAttribute) => handleSwitchWorkspace(id), - render: ({ id }: WorkspaceAttribute) => { - return ( - handleSwitchWorkspace(id)} - iconType="pencil" - size="xs" - color="text" - > - Edit - - ); - }, - }, - { - name: 'Delete', - type: 'button', - description: 'Delete workspace', - 'data-test-subj': 'workspace-list-delete-icon', - render: (item: WorkspaceAttribute) => { - return ( - { - setDeletedWorkspaces([item]); - }} - size="s" - iconType="trash" - color="danger" - style={{ padding: 0 }} - > - Delete - - ); - }, - }, - ], + actions: availableActions, }, ]; - return ( - - - {isDashboardAdmin && renderCreateWorkspaceButton()} - - - setPagination((prev) => { - return { ...prev, pageIndex: index, pageSize: size }; - }) - } - pagination={pagination} - sorting={{ - sort: { - field: initialSortField, - direction: initialSortDirection, + const columns = [...availableColumns, ...actionColumns]; + + const workspaceListTable = ( + + setPagination((prev) => { + return { ...prev, pageIndex: index, pageSize: size }; + }) + } + pagination={pagination} + sorting={{ + sort: { + field: initialSortField, + direction: initialSortDirection, + }, + }} + isSelectable={selectable} + search={searchable ? search : undefined} + selection={selectable ? selectionValue : undefined} + /> + ); + + return fullPage ? ( + <> + + - + {isDashboardAdmin && renderCreateWorkspaceButton()} + + {workspaceListTable} + - {deletedWorkspaces.length > 0 && ( - setDeletedWorkspaces([])} - /> - )} - + {deletedWorkspaces.length > 0 && ( + setDeletedWorkspaces([])} + /> + )} +
+ + ) : ( + workspaceListTable ); }; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 3e04e61a8404..c7d8cb4984b7 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -71,6 +71,7 @@ import { registerAnalyticsAllOverviewContent, setAnalyticsAllOverviewSection, } from './components/use_case_overview/setup_overview'; +import { DefaultWorkspaceList } from './components/workspace_list/default_workspace'; type WorkspaceAppType = ( params: AppMountParameters, @@ -628,6 +629,9 @@ export class WorkspacePlugin // register get started card in new home page this.registerGetStartedCardToNewHome(core, contentManagement); + // register workspace list to user settings page + this.registerWorkspaceListToUserSettings(core, contentManagement, navigation); + // set breadcrumbs enricher for workspace this.breadcrumbsSubscription = enrichBreadcrumbsWithWorkspace(core); @@ -659,6 +663,34 @@ export class WorkspacePlugin } } + private async registerWorkspaceListToUserSettings( + coreStart: CoreStart, + contentManagement: ContentManagementPluginStart, + navigation: NavigationPublicPluginStart + ) { + if (contentManagement) { + const services: Services = { + ...coreStart, + workspaceClient: undefined, + navigationUI: navigation.ui, + }; + contentManagement.registerContentProvider({ + id: 'default_workspace_list', + getContent: () => ({ + id: 'default_workspace_list', + kind: 'custom', + order: 0, + render: () => + React.createElement(DefaultWorkspaceList, { + services, + registeredUseCases$: this.registeredUseCases$, + }), + }), + getTargetArea: () => 'user_settings/default_workspace', + }); + } + } + public stop() { this.currentWorkspaceSubscription?.unsubscribe(); this.currentWorkspaceIdSubscription?.unsubscribe(); diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts index bec84221e2f1..d18556f3dc1d 100644 --- a/src/plugins/workspace/public/types.ts +++ b/src/plugins/workspace/public/types.ts @@ -11,7 +11,7 @@ import { ContentManagementPluginStart } from '../../../plugins/content_managemen import { DataSourceAttributes } from '../../../plugins/data_source/common/data_sources'; export type Services = CoreStart & { - workspaceClient: WorkspaceClient; + workspaceClient: WorkspaceClient | undefined; dataSourceManagement?: DataSourceManagementPluginSetup; navigationUI?: NavigationPublicPluginStart['ui']; contentManagement?: ContentManagementPluginStart; diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index ad45b1f7fa08..8adde466e117 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -43,6 +43,7 @@ import { import { getOSDAdminConfigFromYMLConfig, updateDashboardAdminStateForRequest } from './utils'; import { WorkspaceIdConsumerWrapper } from './saved_objects/workspace_id_consumer_wrapper'; import { WorkspaceUiSettingsClientWrapper } from './saved_objects/workspace_ui_settings_client_wrapper'; +import { uiSettings } from './ui_settings'; export class WorkspacePlugin implements Plugin { private readonly logger: Logger; @@ -172,6 +173,9 @@ export class WorkspacePlugin implements Plugin { + const permissionMode = translatePermissionsToRole( + isPermissionControlEnabled, + workspace.permissions, + principals + ); + workspace.permissionMode = permissionMode; + }); + return res.ok({ body: result, }); diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index b21f31e3accd..ce86b9c2bdd6 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -14,9 +14,10 @@ import { Permissions, UiSettingsServiceStart, } from '../../../core/server'; - +import { PermissionModeId } from '../common/types'; export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { permissions?: Permissions; + permissionMode?: PermissionModeId; } import { WorkspacePermissionMode } from '../common/constants'; @@ -82,7 +83,7 @@ export interface IWorkspaceClientImpl { ): Promise< IResponse< { - workspaces: WorkspaceAttribute[]; + workspaces: WorkspaceAttributeWithPermission[]; } & Pick > >; diff --git a/src/plugins/workspace/server/ui_settings.ts b/src/plugins/workspace/server/ui_settings.ts new file mode 100644 index 000000000000..f34e867cb5f6 --- /dev/null +++ b/src/plugins/workspace/server/ui_settings.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; + +import { UiSettingsParams } from 'opensearch-dashboards/server'; +import { DEFAULT_WORKSPACE } from '../common/constants'; + +export const uiSettings: Record = { + [DEFAULT_WORKSPACE]: { + name: 'Default workspace', + scope: 'user', + value: null, + type: 'string', + schema: schema.nullable(schema.string()), + }, +}; diff --git a/src/plugins/workspace/server/utils.ts b/src/plugins/workspace/server/utils.ts index 9f144c7eb1c3..5515447dba9e 100644 --- a/src/plugins/workspace/server/utils.ts +++ b/src/plugins/workspace/server/utils.ts @@ -20,7 +20,8 @@ import { import { AuthInfo } from './types'; import { updateWorkspaceState } from '../../../core/server/utils'; import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../data_source_management/common'; -import { CURRENT_USER_PLACEHOLDER } from '../common/constants'; +import { CURRENT_USER_PLACEHOLDER, WorkspacePermissionMode } from '../common/constants'; +import { PermissionModeId } from '../common/types'; /** * Generate URL friendly random ID @@ -160,3 +161,51 @@ export const checkAndSetDefaultDataSource = async ( await uiSettingsClient.set(DEFAULT_DATA_SOURCE_UI_SETTINGS_ID, undefined); } }; + +/** + * translate workspace permission object into PermissionModeId + * @param permissions workspace permissions object + * @param isPermissionControlEnabled permission control flag + * @param principals + * @returns PermissionModeId + */ +export const translatePermissionsToRole = ( + isPermissionControlEnabled: boolean, + permissions?: Permissions, + principals?: Principals +): PermissionModeId => { + let permissionMode = PermissionModeId.Owner; + if (isPermissionControlEnabled && permissions) { + const modes = [] as WorkspacePermissionMode[]; + const currentUserId = principals?.users?.[0] || ''; + const currentGroupId = principals?.groups?.[0] || ''; + [ + WorkspacePermissionMode.Write, + WorkspacePermissionMode.LibraryWrite, + WorkspacePermissionMode.LibraryRead, + WorkspacePermissionMode.Read, + ].forEach((mode) => { + if ( + permissions[mode] && + (permissions[mode].users?.includes(currentUserId) || + permissions[mode].groups?.includes(currentGroupId)) + ) { + modes.push(mode); + } + }); + + if ( + modes.includes(WorkspacePermissionMode.LibraryWrite) && + modes.includes(WorkspacePermissionMode.Write) + ) { + permissionMode = PermissionModeId.Owner; + } else if (modes.includes(WorkspacePermissionMode.LibraryWrite)) { + permissionMode = PermissionModeId.ReadAndWrite; + } else { + permissionMode = PermissionModeId.Read; + } + } else { + permissionMode = PermissionModeId.Read; + } + return permissionMode; +}; From 060a5c3988a01520a176b9825b33c2db8cc53b01 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 2 Sep 2024 14:58:02 +0800 Subject: [PATCH 02/21] add unit test Signed-off-by: Hailong Cui --- .../create_or_upgrade_saved_config.test.ts | 1 + .../ui_settings/ui_settings_client.test.ts | 82 +- .../__snapshots__/index.test.tsx.snap | 2392 ++++++++++++++++- .../components/workspace_list/index.test.tsx | 58 +- 4 files changed, 2458 insertions(+), 75 deletions(-) diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index eb23e78b1756..47491488f980 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -155,6 +155,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { Object { "newVersion": "4.0.1", "prevVersion": "4.0.0", + "userLevel": false, }, ], ] diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 8864c52e1cfb..45f13f8e2c5e 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -36,7 +36,7 @@ import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config import { SavedObjectsClient } from '../saved_objects'; import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; -import { UiSettingsClient } from './ui_settings_client'; +import { CURRENT_USER, UiSettingsClient } from './ui_settings_client'; import { CannotOverrideError } from './ui_settings_errors'; const logger = loggingSystemMock.create().get(); @@ -105,6 +105,22 @@ describe('ui settings', () => { }); }); + it('updates several values in one operation includes user level settings', async () => { + const value = chance.word(); + const defaults = { key: { value, scope: 'user' } }; + const { uiSettings, savedObjectsClient } = setup({ defaults }); + await uiSettings.setMany({ one: 'value', another: 'val', key: 'value' }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, ID, { + one: 'value', + another: 'val', + }); + expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`, { + key: 'value', + }); + }); + it('automatically creates the savedConfig if it is missing', async () => { const { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig } = setup(); savedObjectsClient.update @@ -357,8 +373,9 @@ describe('ui settings', () => { const { uiSettings, savedObjectsClient } = setup(); await uiSettings.getUserProvided(); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); }); it('returns user configuration', async () => { @@ -410,6 +427,9 @@ describe('ui settings', () => { Array [ "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", ], + Array [ + "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", + ], ] `); }); @@ -419,16 +439,21 @@ describe('ui settings', () => { savedObjectsClient.get = jest .fn() .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) + .mockResolvedValueOnce({ attributes: {} }) + .mockRejectedValueOnce(SavedObjectsClient.errors.createGenericNotFoundError()) .mockResolvedValueOnce({ attributes: {} }); expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(4); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(2); expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: true }) ); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( + expect.objectContaining({ handleWriteErrors: true, userLevel: true }) + ); }); it('returns result of savedConfig creation in case of notFound error', async () => { @@ -518,8 +543,9 @@ describe('ui settings', () => { const opensearchDocSource = {}; const { uiSettings, savedObjectsClient } = setup({ opensearchDocSource }); await uiSettings.getAll(); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); }); it('returns defaults when opensearch doc is empty', async () => { @@ -552,6 +578,9 @@ describe('ui settings', () => { Array [ "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", ], + Array [ + "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", + ], ] `); }); @@ -599,6 +628,43 @@ describe('ui settings', () => { bar: 'user-provided', }); }); + + // verify user level settings override global + it(`user level values will override global settings`, async () => { + const defaults = { + foo: { + value: 'default', + }, + bar: { + value: 'default', + scope: 'user', + }, + }; + + const { uiSettings, savedObjectsClient } = setup({ defaults }); + + savedObjectsClient.get.mockImplementation((_type, id, _options) => { + if (id === `${CURRENT_USER}_${ID}`) { + return Promise.resolve({ + attributes: { + bar: 'my personal value', + }, + } as any); + } else { + return Promise.resolve({ + attributes: { + foo: 'default1', + bar: 'global value', + }, + } as any); + } + }); + + expect(await uiSettings.getAll()).toStrictEqual({ + foo: 'default1', + bar: 'my personal value', + }); + }); }); describe('#getDefault()', () => { @@ -626,8 +692,9 @@ describe('ui settings', () => { const { uiSettings, savedObjectsClient } = setup({ opensearchDocSource }); await uiSettings.get('any'); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); + expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); }); it(`returns the promised value for a key`, async () => { @@ -722,6 +789,9 @@ describe('ui settings', () => { Array [ "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", ], + Array [ + "Ignore invalid UiSettings value. ValidationError: [validation [id]]: expected value of type [number] but got [string].", + ], ] `); }); diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap index 3c77bc87adc0..3f809e4d146c 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap @@ -1,5 +1,2328 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`WorkspaceList should be able to pagination when clicking pagination button 1`] = ` +
+
+ Organize collaborative projects with use-case-specific workspaces. + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + + + + Use case + + + + + + Description + + + + + + Owners + + + + + + Last updated + + + + + + Data sources + + + + + + Actions + + +
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Owners +
+
+
+
+ Last updated +
+
+ Aug 5, 2024 @ 20:00:00.000 +
+
+
+ Data sources +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+`; + exports[`WorkspaceList should render title and table normally 1`] = `
+
+
+ name1 +
+
+
- +
+
+ name2 +
+
+
- +
+
+ name3 +
+
+
Rows per page : - 5 + 10 diff --git a/src/plugins/workspace/public/components/workspace_list/index.test.tsx b/src/plugins/workspace/public/components/workspace_list/index.test.tsx index bec37c5bb160..af7df32bc68d 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -13,6 +13,7 @@ import { navigateToWorkspaceDetail } from '../utils/workspace'; import { createMockedRegisteredUseCases$ } from '../../mocks'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; import { WorkspaceList } from './index'; +import { exp } from 'mathjs'; jest.mock('../utils/workspace'); @@ -239,57 +240,24 @@ describe('WorkspaceList', () => { }); it('should be able to pagination when clicking pagination button', async () => { - const list = [ - { - id: 'id1', - name: 'name1', - features: ['use-case-all'], - description: '', - lastUpdatedTime: '2024-08-06T00:00:00.00Z', - }, - { - id: 'id2', - name: 'name2', - features: ['use-case-observability'], - description: '', - lastUpdatedTime: '2024-08-06T00:00:00.00Z', - }, - { - id: 'id3', - name: 'name3', - features: ['use-case-search'], - description: '', - lastUpdatedTime: '2024-08-06T00:00:00.00Z', - }, - { - id: 'id4', - name: 'name4', + const list = []; + // add 15 items into list + for (let i = 100; i < 115; i++) { + list.push({ + id: `id${i}`, + name: `name${i}`, features: ['use-case-all'], description: '', - lastUpdatedTime: '2024-08-05T00:00:00.00Z', - }, - { - id: 'id5', - name: 'name5', - features: ['use-case-observability'], - description: '', - lastUpdatedTime: '2024-08-06T00:00:00.00Z', - }, - { - id: 'id6', - name: 'name6', - features: ['use-case-search'], - description: '', lastUpdatedTime: '2024-08-06T00:00:00.00Z', - }, - ]; + }); + } const { getByTestId, getByText, queryByText } = render(getWrapWorkspaceListInContext(list)); - expect(getByText('name1')).toBeInTheDocument(); - expect(queryByText('name6')).not.toBeInTheDocument(); + expect(getByText('name100')).toBeInTheDocument(); + expect(queryByText('name110')).not.toBeInTheDocument(); const paginationButton = getByTestId('pagination-button-next'); fireEvent.click(paginationButton); - expect(queryByText('name1')).not.toBeInTheDocument(); - expect(getByText('name6')).toBeInTheDocument(); + expect(queryByText('name100')).not.toBeInTheDocument(); + expect(queryByText('name110')).toBeInTheDocument(); }); it('should display create workspace button for dashboard admin', async () => { From de214ec8cf942bb2229eb35fafd5e0b0fac02013 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 00:31:53 +0800 Subject: [PATCH 03/21] add unit test Signed-off-by: Hailong Cui --- .../management_app/user_settings.test.tsx | 103 +++ .../public/management_app/user_settings.tsx | 4 +- .../advanced_settings/public/plugin.test.ts | 23 + .../advanced_settings/public/plugin.ts | 2 +- .../server/capabilities_provider.ts | 2 +- .../advanced_settings/server/plugin.ts | 2 +- .../user_ui_settings_client_wrapper.test.ts | 228 +++++ .../user_ui_settings_client_wrapper.ts | 4 +- .../user_ui_settings_client_wrapper.ts.bak | 189 ----- .../advanced_settings/server/utils.test.ts | 53 ++ .../default_workspace.test.tsx.snap | 795 ++++++++++++++++++ .../workspace_list/default_workspace.test.tsx | 154 ++++ .../workspace_list/default_workspace.tsx | 2 +- .../components/workspace_list/index.test.tsx | 1 - src/plugins/workspace/public/plugin.ts | 4 +- src/plugins/workspace/public/types.ts | 2 +- 16 files changed, 1366 insertions(+), 202 deletions(-) create mode 100644 src/plugins/advanced_settings/public/management_app/user_settings.test.tsx create mode 100644 src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts delete mode 100644 src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak create mode 100644 src/plugins/advanced_settings/server/utils.test.ts create mode 100644 src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap create mode 100644 src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx diff --git a/src/plugins/advanced_settings/public/management_app/user_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/user_settings.test.tsx new file mode 100644 index 000000000000..56889915247e --- /dev/null +++ b/src/plugins/advanced_settings/public/management_app/user_settings.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { contentManagementPluginMocks } from '../../../content_management/public'; +import { setupUserSettingsPage, UserSettingsApp } from './user_settings'; +import { I18nProvider } from '@osd/i18n/react'; +import { coreMock } from '../../../../core/public/mocks'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { render } from '@testing-library/react'; + +describe('UserSettings', () => { + const registerPageMock = jest.fn(); + const contentManagementSetupMock = { + ...contentManagementPluginMocks.createSetupContract(), + registerPage: registerPageMock, + }; + + const coreStartMock = coreMock.createStart(); + const renderPageMock = jest.fn(); + + renderPageMock.mockReturnValue('
page content
'); + + const contentManagementStartMock = { + ...contentManagementPluginMocks.createStartContract(), + renderPage: renderPageMock, + }; + + const mockHeaderControl = ({ controls }) => { + return controls?.[0].description ?? controls?.[0].renderComponent ?? null; + }; + + function renderUserSettingsApp() { + const services = { + ...coreStartMock, + navigation: { + ui: { + HeaderControl: mockHeaderControl, + }, + }, + contentManagement: contentManagementStartMock, + }; + + return ( + + + + + + ); + } + + it('setupUserSettingsPage', () => { + setupUserSettingsPage(contentManagementSetupMock); + + const calls = registerPageMock.mock.calls[0]; + expect(calls[0]).toMatchInlineSnapshot(` + Object { + "id": "user_settings", + "sections": Array [ + Object { + "id": "user_profile", + "kind": "custom", + "order": 1000, + "render": [Function], + "title": "User's profile", + }, + Object { + "id": "default_workspace", + "kind": "custom", + "order": 2000, + "render": [Function], + }, + Object { + "id": "user_identity_role", + "kind": "custom", + "order": 3000, + "render": [Function], + }, + ], + "title": "User Settings", + } + `); + }); + + it('renders', () => { + const { container } = render(renderUserSettingsApp()); + expect(container).toMatchInlineSnapshot(` +
+
+ Configure your personal preferences. + <div>page content</div> +
+
+ `); + expect(renderPageMock).toHaveBeenCalledWith('user_settings', { fragmentOnly: true }); + expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/advanced_settings/public/management_app/user_settings.tsx b/src/plugins/advanced_settings/public/management_app/user_settings.tsx index d0a1424fd585..bb3d1118f59a 100644 --- a/src/plugins/advanced_settings/public/management_app/user_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/user_settings.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBreadcrumb, EuiPage, EuiPanel } from '@elastic/eui'; +import { EuiBreadcrumb, EuiPage } from '@elastic/eui'; import { CoreStart } from 'opensearch-dashboards/public'; import React, { useEffect } from 'react'; import { i18n } from '@osd/i18n'; @@ -32,7 +32,7 @@ const sectionRender = (contents: Content[]) => { export const setupUserSettingsPage = (contentManagement?: ContentManagementPluginSetup) => { contentManagement?.registerPage({ id: 'user_settings', - title: 'Home', + title: 'User Settings', sections: [ { id: 'user_profile', diff --git a/src/plugins/advanced_settings/public/plugin.test.ts b/src/plugins/advanced_settings/public/plugin.test.ts index 2ff2a08b8077..b018e960f383 100644 --- a/src/plugins/advanced_settings/public/plugin.test.ts +++ b/src/plugins/advanced_settings/public/plugin.test.ts @@ -21,3 +21,26 @@ describe('AdvancedSettingsPlugin', () => { expect(setupMock.application.register).toBeCalledTimes(1); }); }); + +describe('UserSettingsPlugin', () => { + it('setup successfully', () => { + const pluginInstance = new AdvancedSettingsPlugin(); + const setupMock = { + ...coreMock.createSetup(), + chrome: { + ...coreMock.createSetup().chrome, + navGroup: { + ...coreMock.createSetup().chrome.navGroup, + getNavGroupEnabled: () => true, + }, + }, + }; + expect(() => + pluginInstance.setup(setupMock, { + management: managementPluginMock.createSetupContract(), + home: homePluginMock.createSetupContract(), + }) + ).not.toThrow(); + expect(setupMock.application.register).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index d3143d0e2397..6ea960712046 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -191,7 +191,7 @@ export class AdvancedSettingsPlugin } this.appUpdater$.next((app) => { - const securityEnabled = core.application.capabilities.useSettings?.enabled; + const securityEnabled = core.application.capabilities.userSettings?.enabled; if (app.id === 'user_settings') { return { navLinkStatus: securityEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, diff --git a/src/plugins/advanced_settings/server/capabilities_provider.ts b/src/plugins/advanced_settings/server/capabilities_provider.ts index 9b78577a0948..c8210c029faa 100644 --- a/src/plugins/advanced_settings/server/capabilities_provider.ts +++ b/src/plugins/advanced_settings/server/capabilities_provider.ts @@ -33,7 +33,7 @@ export const capabilitiesProvider = () => ({ show: true, save: true, }, - useSettings: { + userSettings: { enabled: false, }, }); diff --git a/src/plugins/advanced_settings/server/plugin.ts b/src/plugins/advanced_settings/server/plugin.ts index bd9993d0ecd2..a7b478920062 100644 --- a/src/plugins/advanced_settings/server/plugin.ts +++ b/src/plugins/advanced_settings/server/plugin.ts @@ -85,7 +85,7 @@ export class AdvancedSettingsServerPlugin implements Plugin { if (userName) { return { ...capabilities, - useSettings: { + userSettings: { enabled: true, }, }; diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts new file mode 100644 index 000000000000..d48dbe9da2d3 --- /dev/null +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts @@ -0,0 +1,228 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks'; +import { UserUISettingsClientWrapper } from './user_ui_settings_client_wrapper'; +// eslint-disable-next-line @osd/eslint/no-restricted-paths +import { loggerMock } from '../../../../core/server/logging/logger.mock'; +import { CURRENT_USER } from '../../../../core/server'; + +jest.mock('../utils', () => { + return { + extractUserName: jest.fn().mockReturnValue('test_user'), + }; +}); + +describe('UserUISettingsClientWrapper', () => { + const requestHandlerContext = coreMock.createRequestHandlerContext(); + const mockedClient = savedObjectsClientMock.create(); + const requestMock = httpServerMock.createOpenSearchDashboardsRequest(); + + const buildWrapperInstance = (permissionEnabled: boolean) => { + const wrapperInstance = new UserUISettingsClientWrapper(loggerMock.create(), permissionEnabled); + const wrapperClient = wrapperInstance.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: requestMock, + }); + return wrapperClient; + }; + + const wrapperClient = buildWrapperInstance(false); + + describe('#get', () => { + // beforeEach + beforeEach(() => { + jest.clearAllMocks(); + }); + + // test getUiSettings + it('should skip replacing user id if not user level config', async () => { + await wrapperClient.get('config', '3.0.0'); + + expect(mockedClient.get).toBeCalledWith('config', '3.0.0', {}); + }); + + it('should skip replacing user id if type is not config', async () => { + await wrapperClient.get('config1', `${CURRENT_USER}_3.0.0`); + + expect(mockedClient.get).toBeCalledWith('config1', `${CURRENT_USER}_3.0.0`, {}); + }); + + it('should replace user id placeholder with real user id', async () => { + await wrapperClient.get('config', `${CURRENT_USER}_3.0.0`); + + expect(mockedClient.get).toBeCalledWith('config', 'test_user_3.0.0', {}); + }); + }); + + describe('#update', () => { + // beforeEach + beforeEach(() => { + jest.clearAllMocks(); + }); + + // test getUiSettings + it('should skip replacing user id if not user level config', async () => { + await wrapperClient.update('config', '3.0.0', {}); + + expect(mockedClient.update).toBeCalledWith('config', '3.0.0', {}, {}); + }); + + it('should skip replacing user id if type is not config', async () => { + await wrapperClient.update('config1', `${CURRENT_USER}_3.0.0`, {}); + + expect(mockedClient.update).toBeCalledWith('config1', `${CURRENT_USER}_3.0.0`, {}, {}); + }); + + it('should replace user id placeholder with real user id', async () => { + await wrapperClient.update('config', `${CURRENT_USER}_3.0.0`, {}); + + expect(mockedClient.update).toBeCalledWith('config', 'test_user_3.0.0', {}, {}); + }); + }); + + describe('#create', () => { + // beforeEach + beforeEach(() => { + jest.clearAllMocks(); + }); + + // test getUiSettings + it('should skip replacing user id if not user level config', async () => { + await wrapperClient.create('config', {}, { id: '3.0.0' }); + + expect(mockedClient.create).toBeCalledWith('config', {}, { id: '3.0.0' }); + }); + + it('should skip replacing user id if type is not config', async () => { + await wrapperClient.create('config1', {}, { id: `${CURRENT_USER}_3.0.0` }); + + expect(mockedClient.create).toBeCalledWith('config1', {}, { id: `${CURRENT_USER}_3.0.0` }); + }); + + it('should replace user id placeholder with real user id', async () => { + await wrapperClient.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + + expect(mockedClient.create).toBeCalledWith( + 'config', + {}, + { + id: 'test_user_3.0.0', + references: [ + { + id: 'test_user', + name: 'test_user', + type: 'user', + }, + ], + } + ); + }); + + it('should replace user id placeholder with real user id and permission enabled', async () => { + const wrapperClientWithPermission = buildWrapperInstance(true); + await wrapperClientWithPermission.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + + expect(mockedClient.create).toBeCalledWith( + 'config', + {}, + { + id: 'test_user_3.0.0', + references: [ + { + id: 'test_user', + name: 'test_user', + type: 'user', + }, + ], + permissions: { + write: { + users: ['test_user'], + }, + }, + } + ); + }); + }); + + describe('#find', () => { + mockedClient.find.mockResolvedValue({ + saved_objects: [ + { + score: 1, + id: 'test_user_4.0.0', + type: 'config', + attributes: {}, + references: [], + }, + { + score: 1, + id: 'test_user_4.1.0', + type: 'config', + attributes: {}, + references: [], + }, + ], + total: 2, + per_page: 10, + page: 1, + }); + // beforeEach + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should replace user id', async () => { + const resp = await wrapperClient.find({ + type: 'config', + hasReference: { + type: 'user', + id: CURRENT_USER, + }, + }); + + expect(resp.saved_objects).toHaveLength(2); + expect(resp.saved_objects[0].id).toEqual('4.0.0'); + expect(resp.saved_objects[1].id).toEqual('4.1.0'); + + expect(mockedClient.find).toBeCalledWith({ + type: 'config', + hasReference: { + type: 'user', + id: 'test_user', + }, + }); + }); + + it('should not replace user id if not hasReference', async () => { + await wrapperClient.find({ + type: 'config', + }); + + expect(mockedClient.find).toBeCalledWith({ + type: 'config', + }); + }); + + it('should not replace user id if hasReference do not contains user id placeholder', async () => { + await wrapperClient.find({ + type: 'config', + hasReference: { + type: 'user', + id: 'test', + }, + }); + + expect(mockedClient.find).toBeCalledWith({ + type: 'config', + hasReference: { + type: 'user', + id: 'test', + }, + }); + }); + }); +}); diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index 5db43f92d819..709441a7c142 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -131,9 +131,7 @@ export class UserUISettingsClientWrapper { so.id = so.id.replace(`${userName}_`, ''); }); - return new Promise((resolve) => { - resolve(resp); - }); + return Promise.resolve(resp); } return wrapperOptions.client.find(options); }; diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak deleted file mode 100644 index 6aafae1987e8..000000000000 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts.bak +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - SavedObject, - SavedObjectsBaseOptions, - SavedObjectsClientWrapperFactory, - SavedObjectsUpdateOptions, - SavedObjectsUpdateResponse, - CoreStart, - AuthStatus, - SavedObjectsErrorHelpers, - OpenSearchDashboardsRequest, - ACL, - SavedObjectsCreateOptions, -} from '../../../../core/server'; -import { Logger } from '../../../../core/server'; - -export interface AuthInfo { - backend_roles?: string[]; - user_name?: string; -} - -export const hashUserName = (username: string) => { - return username; -}; - -/** - * This saved object client wrapper offers methods to get and update UI settings considering - * the context of the current user - */ -export class UserUISettingsClientWrapper { - constructor( - private readonly logger: Logger, - private readonly savedObjectsPermissionEnabled: boolean - ) {} - private core: CoreStart | undefined; - - public setCore(core: CoreStart) { - this.core = core; - } - - private extractUserName(request: OpenSearchDashboardsRequest) { - const authInfoResp = this.core?.http.auth.get(request); - - if (authInfoResp?.status === AuthStatus.authenticated) { - const authInfo = authInfoResp?.state as { authInfo: AuthInfo } | null; - return authInfo?.authInfo?.user_name; - } - return undefined; - } - - public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { - const getUiSettings = async ( - type: string, - id: string, - options: SavedObjectsBaseOptions = {} - ): Promise> => { - const userName = this.extractUserName(wrapperOptions.request); - - /** - * When getting ui settings within a workspace, it will combine the workspace ui settings with - * the global ui settings and workspace ui settings have higher priority if the same setting - * was defined in both places - */ - if (type === 'config' && userName) { - this.logger.debug(`getting ui settings for ${userName} with id ${id}`); - // global value - const configObject = await wrapperOptions.client.get>( - 'config', - id, - options - ); - try { - // user level - const userConfigObject = await wrapperOptions.client.get>( - 'config', - hashUserName(userName), - options - ); - - configObject.attributes = { - ...configObject.attributes, - ...(userConfigObject ? userConfigObject.attributes : {}), - }; - } catch (err) { - // ignore - } - - return configObject as SavedObject; - } - - return wrapperOptions.client.get(type, id, options); - }; - - const updateUiSettings = async ( - type: string, - id: string, - attributes: Partial, - options: SavedObjectsUpdateOptions = {} - ): Promise> => { - const userName = this.extractUserName(wrapperOptions.request); - - if (type === 'config' && userName && this.core) { - const allRegisteredSettings = this.core.uiSettings - .asScopedToClient(wrapperOptions.client) - .getRegistered(); - - const userLevelKeys = [] as string[]; - Object.keys(allRegisteredSettings).forEach((key) => { - const setting = allRegisteredSettings[key]; - if (setting.scope === 'user') { - userLevelKeys.push(key); - } - }); - - const userLevelSettings = {} as Partial; - const globalSettings = {} as Partial; - - Object.entries(attributes).forEach(([key, value]) => { - if (userLevelKeys.includes(key)) { - Object.assign(userLevelSettings, { [key]: value }); - } else { - Object.assign(globalSettings, { [key]: value }); - } - }); - - let updateRes: SavedObjectsUpdateResponse = { - attributes, - references: undefined, - id, - type: 'config', - }; - - const docId = hashUserName(userName); - - // update user level settings - if (Object.keys(userLevelSettings).length > 0) { - try { - await wrapperOptions.client.update(type, docId, userLevelSettings, options); - } catch (error) { - if (!SavedObjectsErrorHelpers.isNotFoundError(error)) { - throw error; - } - - let createOptions: SavedObjectsCreateOptions = { id: docId }; - if (this.savedObjectsPermissionEnabled) { - const permissionObject = new ACL() - .addPermission(['write'], { - users: [userName], - }) - .getPermissions(); - - createOptions = { ...options, permissions: permissionObject }; - } - - await wrapperOptions.client.create(type, userLevelSettings, createOptions); - } - } - - // update global ui settings - if (Object.keys(globalSettings).length > 0) { - updateRes = await wrapperOptions.client.update(type, id, globalSettings, options); - } - return updateRes; - } - return wrapperOptions.client.update(type, id, attributes, options); - }; - - return { - ...wrapperOptions.client, - checkConflicts: wrapperOptions.client.checkConflicts, - errors: wrapperOptions.client.errors, - addToNamespaces: wrapperOptions.client.addToNamespaces, - deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, - find: wrapperOptions.client.find, - bulkGet: wrapperOptions.client.bulkGet, - create: wrapperOptions.client.create, - bulkCreate: wrapperOptions.client.bulkCreate, - delete: wrapperOptions.client.delete, - bulkUpdate: wrapperOptions.client.bulkUpdate, - deleteByWorkspace: wrapperOptions.client.deleteByWorkspace, - get: getUiSettings, - update: updateUiSettings, - }; - }; -} diff --git a/src/plugins/advanced_settings/server/utils.test.ts b/src/plugins/advanced_settings/server/utils.test.ts new file mode 100644 index 000000000000..f0089afe51af --- /dev/null +++ b/src/plugins/advanced_settings/server/utils.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock, httpServerMock } from '../../../core/server/mocks'; +import { extractUserName } from './utils'; +import { AuthStatus } from '../../../core/server'; + +// help on write unit test on utils.ts with jest +describe('utils', () => { + const isAuthenticatedMock = jest.fn().mockReturnValue(true); + const getMock = jest.fn().mockReturnValue({ + status: AuthStatus.authenticated, + state: { + authInfo: { + user_name: 'test_user', + }, + }, + }); + const coreStartMock = { + ...coreMock.createStart(), + http: { + ...coreMock.createStart().http, + auth: { + ...coreMock.createStart().http.auth, + get: getMock, + isAuthenticated: isAuthenticatedMock, + }, + }, + }; + const requestMock = httpServerMock.createOpenSearchDashboardsRequest(); + + // test extractUserName + it('extractUserName when authenticated', () => { + const result = extractUserName(requestMock, coreStartMock); + expect(result).toBe('test_user'); + }); + + it('extractUserName when not authenticated', () => { + isAuthenticatedMock.mockReturnValue(false); + getMock.mockReturnValue({ status: AuthStatus.unauthenticated }); + const result = extractUserName(requestMock, coreStartMock); + expect(result).toBeFalsy(); + }); + + it('extractUserName when auth status is unknown', () => { + isAuthenticatedMock.mockReturnValue(false); + getMock.mockReturnValue({ status: AuthStatus.unknown }); + const result = extractUserName(requestMock, coreStartMock); + expect(result).toBeFalsy(); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap new file mode 100644 index 000000000000..dd1be51cf57f --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap @@ -0,0 +1,795 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserDefaultWorkspace should render title and table normally 1`] = ` +
+
+
+
+
+

+ Workspaces ( + 3 + ) +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + Use case + + + + + + Description + + + + + + Permissions + + + + + + Actions + + +
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Analytics (All) + +
+
+
+ Description +
+
+ +
+ should be able to see the description tooltip when hovering over the description +
+
+
+
+
+ Permissions +
+
+ +
+
+
+ + + +
+ Copy ID +
+
+
+ + + +
+ Set as default +
+
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Observability + +
+
+
+ Description +
+
+ +
+ should be able to see the description tooltip when hovering over the description +
+
+
+
+
+ Permissions +
+
+ +
+
+
+ + + +
+ Copy ID +
+
+
+ + + +
+ Set as default +
+
+
+
+
+
+ Name +
+
+ + + +
+
+
+ Use case +
+
+ + Search + +
+
+
+ Description +
+
+ +
+ +
+
+
+ Permissions +
+
+ +
+
+
+ + + +
+ Copy ID +
+
+
+ + + +
+ Set as default +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+`; diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx new file mode 100644 index 000000000000..20a2afda0216 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx @@ -0,0 +1,154 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import moment from 'moment'; +import { BehaviorSubject, of } from 'rxjs'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; +import { coreMock } from '../../../../../core/public/mocks'; +import { createMockedRegisteredUseCases$ } from '../../mocks'; +import { UserDefaultWorkspace } from './default_workspace'; +import { WorkspaceClient } from '../../workspace_client'; +import { WorkspaceObject } from '../../../../../core/public'; + +jest.mock('../utils/workspace'); +jest.mock('../../utils', () => { + const original = jest.requireActual('../../utils'); + return { + ...original, + getDataSourcesList: jest.fn().mockResolvedValue(() => [ + { + id: 'ds_id1', + title: 'ds_title1', + workspaces: 'id1', + }, + { + id: 'ds_id2', + title: 'ds_title2', + workspaces: 'id1', + }, + { + id: 'ds_id3', + title: 'ds_title3', + workspaces: 'id1', + }, + ]), + }; +}); + +function getWrapUserDefaultWorkspaceList( + workspaceList = [ + { + id: 'id1', + name: 'name1', + features: ['use-case-all'], + description: + 'should be able to see the description tooltip when hovering over the description', + lastUpdatedTime: '1999-08-06T02:00:00.00Z', + permissions: { + write: { + users: ['admin', 'nonadmin'], + }, + }, + }, + { + id: 'id2', + name: 'name2', + features: ['use-case-observability'], + description: + 'should be able to see the description tooltip when hovering over the description', + lastUpdatedTime: '1999-08-06T00:00:00.00Z', + }, + { + id: 'id3', + name: 'name3', + features: ['use-case-search'], + description: '', + lastUpdatedTime: '1999-08-06T01:00:00.00Z', + }, + ], + isDashboardAdmin = true +) { + const coreStartMock = coreMock.createStart(); + const coreSetupMock = coreMock.createSetup(); + coreStartMock.application.capabilities = { + ...coreStartMock.application.capabilities, + dashboards: { + isDashboardAdmin, + }, + }; + + const mockHeaderControl = ({ controls }) => { + return controls?.[0].description ?? controls?.[0].renderComponent ?? null; + }; + + const workspaceClientMock = new WorkspaceClient(coreSetupMock.http, coreSetupMock.workspaces); + + const services = { + ...coreStartMock, + workspaces: { + ...coreStartMock.workspaces, + workspaceList$: new BehaviorSubject(workspaceList), + }, + uiSettings: { + ...coreStartMock.uiSettings, + get: jest.fn().mockImplementation((key) => { + if (key === 'dateFormat') { + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + } + return null; + }), + }, + navigationUI: { + HeaderControl: mockHeaderControl, + }, + workspaceClient: workspaceClientMock, + }; + + return ( + + + + ); +} + +describe('UserDefaultWorkspace', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render title and table normally', () => { + const { getByText, getByRole, container } = render(getWrapUserDefaultWorkspaceList()); + expect(getByText('Workspaces (3)')).toBeInTheDocument(); + expect(getByRole('table')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + + it('should render data in table based on workspace list data', async () => { + const { getByText, queryByText, queryAllByText } = render(getWrapUserDefaultWorkspaceList()); + + // should display workspace names + expect(getByText('name1')).toBeInTheDocument(); + expect(getByText('name2')).toBeInTheDocument(); + + // should display use case + expect(getByText('Analytics (All)')).toBeInTheDocument(); + expect(getByText('Observability')).toBeInTheDocument(); + + // owner column not display + expect(queryByText('admin')).not.toBeInTheDocument(); + + // euiTableRow-isSelectable + expect(document.querySelectorAll('.euiTableRow-isSelectable').length).toBe(0); + + // action button Set as default in document + expect(queryAllByText('Set as default')).toHaveLength(3); + expect(queryAllByText('Copy ID')).toHaveLength(3); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx index 8008d47dc691..126190332065 100644 --- a/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.tsx @@ -16,7 +16,7 @@ interface Props { registeredUseCases$: BehaviorSubject; } -export const DefaultWorkspaceList = ({ services, registeredUseCases$ }: Props) => { +export const UserDefaultWorkspace = ({ services, registeredUseCases$ }: Props) => { const { workspaces } = services; const workspaceList = useObservable(workspaces?.workspaceList$ ?? of([]), []); diff --git a/src/plugins/workspace/public/components/workspace_list/index.test.tsx b/src/plugins/workspace/public/components/workspace_list/index.test.tsx index af7df32bc68d..a945866fd7bf 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -13,7 +13,6 @@ import { navigateToWorkspaceDetail } from '../utils/workspace'; import { createMockedRegisteredUseCases$ } from '../../mocks'; import { OpenSearchDashboardsContextProvider } from '../../../../../plugins/opensearch_dashboards_react/public'; import { WorkspaceList } from './index'; -import { exp } from 'mathjs'; jest.mock('../utils/workspace'); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index c7d8cb4984b7..51ab93b523b8 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -71,7 +71,7 @@ import { registerAnalyticsAllOverviewContent, setAnalyticsAllOverviewSection, } from './components/use_case_overview/setup_overview'; -import { DefaultWorkspaceList } from './components/workspace_list/default_workspace'; +import { UserDefaultWorkspace } from './components/workspace_list/default_workspace'; type WorkspaceAppType = ( params: AppMountParameters, @@ -681,7 +681,7 @@ export class WorkspacePlugin kind: 'custom', order: 0, render: () => - React.createElement(DefaultWorkspaceList, { + React.createElement(UserDefaultWorkspace, { services, registeredUseCases$: this.registeredUseCases$, }), diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts index d18556f3dc1d..bec84221e2f1 100644 --- a/src/plugins/workspace/public/types.ts +++ b/src/plugins/workspace/public/types.ts @@ -11,7 +11,7 @@ import { ContentManagementPluginStart } from '../../../plugins/content_managemen import { DataSourceAttributes } from '../../../plugins/data_source/common/data_sources'; export type Services = CoreStart & { - workspaceClient: WorkspaceClient | undefined; + workspaceClient: WorkspaceClient; dataSourceManagement?: DataSourceManagementPluginSetup; navigationUI?: NavigationPublicPluginStart['ui']; contentManagement?: ContentManagementPluginStart; From c2dd98e9aaed3bb710eb06574541861fdff13ebe Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:39:57 +0000 Subject: [PATCH 04/21] Changeset file for PR #7953 created/updated --- changelogs/fragments/7953.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7953.yml diff --git a/changelogs/fragments/7953.yml b/changelogs/fragments/7953.yml new file mode 100644 index 000000000000..fae54257ade7 --- /dev/null +++ b/changelogs/fragments/7953.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace] Support user personal settings ([#7953](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7953)) \ No newline at end of file From 80fc1dcec2adefe1ca19caee66e81e71ea0bfc4a Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 00:51:35 +0800 Subject: [PATCH 05/21] replace with constant DEFAULT_WORKSPACE Signed-off-by: Hailong Cui --- src/plugins/workspace/server/plugin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index 8adde466e117..7b0702b41f71 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -24,6 +24,7 @@ import { PRIORITY_FOR_WORKSPACE_UI_SETTINGS_WRAPPER, WORKSPACE_INITIAL_APP_ID, WORKSPACE_NAVIGATION_APP_ID, + DEFAULT_WORKSPACE, } from '../common/constants'; import { IWorkspaceClientImpl, WorkspacePluginSetup, WorkspacePluginStart } from './types'; import { WorkspaceClient } from './workspace_client'; @@ -132,11 +133,10 @@ export class WorkspacePlugin implements Plugin workspace.id === defaultWorkspaceId ); From b3e2acf2cfc78169234529ce9d8850e93b4cf3ea Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 10:37:26 +0800 Subject: [PATCH 06/21] update test snapshot Signed-off-by: Hailong Cui --- .../__snapshots__/index.test.tsx.snap | 2323 ----------------- src/plugins/workspace/public/plugin.test.ts | 2 + src/plugins/workspace/public/plugin.ts | 4 +- 3 files changed, 5 insertions(+), 2324 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap index 3f809e4d146c..498eeb9e0198 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap @@ -1,2328 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`WorkspaceList should be able to pagination when clicking pagination button 1`] = ` -
-
- Organize collaborative projects with use-case-specific workspaces. - -
-
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
- -
- -
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -
-
-
-
- - - - - Use case - - - - - - Description - - - - - - Owners - - - - - - Last updated - - - - - - Data sources - - - - - - Actions - - -
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
- -
-
-
-
-
- Name -
-
- - - -
-
-
- Use case -
-
- - Analytics (All) - -
-
-
- Description -
-
- -
- -
-
-
- Owners -
-
-
-
- Last updated -
-
- Aug 5, 2024 @ 20:00:00.000 -
-
-
- Data sources -
-
-
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
-
-`; - exports[`WorkspaceList should render title and table normally 1`] = `
{ const mockDependencies: WorkspacePluginStartDeps = { contentManagement: contentManagementPluginMocks.createStartContract(), + navigation: navigationPluginMock.createStartContract(), }; const getSetupMock = () => coreMock.createSetup(); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 51ab93b523b8..98eb83bd15d8 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -105,6 +105,7 @@ export class WorkspacePlugin private registeredUseCasesUpdaterSubscription?: Subscription; private workspaceAndUseCasesCombineSubscription?: Subscription; private useCase = new UseCaseService(); + private workspaceClient?: WorkspaceClient; private _changeSavedObjectCurrentWorkspace() { if (this.coreStart) { @@ -262,6 +263,7 @@ export class WorkspacePlugin ) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); await workspaceClient.init(); + this.workspaceClient = workspaceClient; core.workspaces.setClient(workspaceClient); this.useCase.setup({ @@ -671,7 +673,7 @@ export class WorkspacePlugin if (contentManagement) { const services: Services = { ...coreStart, - workspaceClient: undefined, + workspaceClient: this.workspaceClient!, navigationUI: navigation.ui, }; contentManagement.registerContentProvider({ From 32becd6e4571d11f8adaca077af872990e2e1e02 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 11:43:49 +0800 Subject: [PATCH 07/21] update incorrect type in test data Signed-off-by: Hailong Cui --- .../functional/fixtures/opensearch_archiver/visualize/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/fixtures/opensearch_archiver/visualize/data.json b/test/functional/fixtures/opensearch_archiver/visualize/data.json index b3245fb765e6..f9250c37f61a 100644 --- a/test/functional/fixtures/opensearch_archiver/visualize/data.json +++ b/test/functional/fixtures/opensearch_archiver/visualize/data.json @@ -206,7 +206,7 @@ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", "title": "test_index*" }, - "type": "test_index*" + "type": "index-pattern" } } } From cabf38a8ee2acc2ad18b1e2a0bf39f10f4c72009 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 12:04:57 +0800 Subject: [PATCH 08/21] fix lint Signed-off-by: Hailong Cui --- src/plugins/workspace/public/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 1eb0e1292840..936244623bde 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -72,7 +72,6 @@ import { import { UserDefaultWorkspace } from './components/workspace_list/default_workspace'; import { registerGetStartedCardToNewHome } from './components/home_get_start_card'; - type WorkspaceAppType = ( params: AppMountParameters, services: Services, From 6776cdc16d4734117ec3362e544c8a5b00fb6696 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 14:47:08 +0800 Subject: [PATCH 09/21] replace user placeholder when security not enabled Signed-off-by: Hailong Cui --- .../user_ui_settings_client_wrapper.ts | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index 709441a7c142..c7c3fa74bce8 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -40,8 +40,12 @@ export class UserUISettingsClientWrapper { private normalizeDocId(id: string, request: OpenSearchDashboardsRequest, core?: CoreStart) { const userName = extractUserName(request, core); - if (userName && this.isUserLevelSetting(id)) { - return id.replace(CURRENT_USER, userName); + if (this.isUserLevelSetting(id)) { + if (userName) { + return id.replace(CURRENT_USER, userName); + } else { + return id.replace(`${CURRENT_USER}_`, ''); + } } return id; } @@ -86,29 +90,37 @@ export class UserUISettingsClientWrapper { const { id } = options || {}; const userLevel = this.isUserLevelSetting(id); - if (type === 'config' && userLevel && userName && id) { - const permissions = { - permissions: new ACL() - .addPermission(['write'], { - users: [userName], - }) - .getPermissions()!, - }; - + if (type === 'config' && id) { const docId = this.normalizeDocId(id, wrapperOptions.request, this.core); - // create with reference, the reference field will used for filter settings by user - return await wrapperOptions.client.create(type, attributes, { - ...options, - id: docId, - references: [ - { - type: 'user', // dummy type - id: userName, - name: userName, - }, - ], - ...(this.savedObjectsPermissionEnabled ? permissions : {}), - }); + + if (userLevel && userName) { + const permissions = { + permissions: new ACL() + .addPermission(['write'], { + users: [userName], + }) + .getPermissions()!, + }; + + // create with reference, the reference field will used for filter settings by user + return await wrapperOptions.client.create(type, attributes, { + ...options, + id: docId, + references: [ + { + type: 'user', // dummy type + id: userName, + name: userName, + }, + ], + ...(this.savedObjectsPermissionEnabled ? permissions : {}), + }); + } else { + return wrapperOptions.client.create(type, attributes, { + ...options, + id: docId, + }); + } } return wrapperOptions.client.create(type, attributes, options); }; From 9dee281e237510081f7a9c1c2dbe745914ee96bd Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 3 Sep 2024 18:16:42 +0800 Subject: [PATCH 10/21] remove version for user level settings Signed-off-by: Hailong Cui --- .../ui_settings/ui_settings_client.test.ts | 7 +- .../server/ui_settings/ui_settings_client.ts | 24 +++++-- .../user_ui_settings_client_wrapper.test.ts | 65 +++++++++++++++++-- .../user_ui_settings_client_wrapper.ts | 9 ++- 4 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 45f13f8e2c5e..55d828bb1c53 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -445,15 +445,12 @@ describe('ui settings', () => { expect(await uiSettings.getUserProvided()).toStrictEqual({}); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(4); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(3); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(2); + expect(createOrUpgradeSavedConfig).toHaveBeenCalledTimes(1); expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( expect.objectContaining({ handleWriteErrors: true }) ); - expect(createOrUpgradeSavedConfig).toHaveBeenCalledWith( - expect.objectContaining({ handleWriteErrors: true, userLevel: true }) - ); }); it('returns result of savedConfig creation in case of notFound error', async () => { diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index aeef5c843930..43194fcf6778 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -53,6 +53,7 @@ interface ReadOptions { ignore401Errors?: boolean; autoCreateOrUpgradeIfMissing?: boolean; userLevel?: boolean; + ignore404Errors?: boolean; } interface UserProvidedValue { @@ -124,9 +125,16 @@ export class UiSettingsClient implements IUiSettingsClient { async getUserProvided(): Promise> { // user provided for global const userProvided: UserProvided = this.onReadHook(await this.read()); - // personal level settings - const personalLevelProvided: UserProvided = this.onReadHook( - await this.read({ userLevel: true }) + /** + * user personal settings, which is not versioned, so that we don't need to create when get + * and ignore 404 when not found the document + */ + const userLevelProvided: UserProvided = this.onReadHook( + await this.read({ + userLevel: true, + autoCreateOrUpgradeIfMissing: false, + ignore404Errors: true, + }) ); // write all overridden keys, dropping the userValue is override is null and @@ -135,11 +143,11 @@ export class UiSettingsClient implements IUiSettingsClient { userProvided[key] = value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; - personalLevelProvided[key] = + userLevelProvided[key] = value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } - return { ...userProvided, ...personalLevelProvided }; + return { ...userProvided, ...userLevelProvided }; } async setMany(changes: Record) { @@ -287,6 +295,7 @@ export class UiSettingsClient implements IUiSettingsClient { ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, userLevel = false, + ignore404Errors = false, }: ReadOptions = {}): Promise> { let docId = this.id; if (userLevel) { @@ -317,6 +326,11 @@ export class UiSettingsClient implements IUiSettingsClient { return failedUpgradeAttributes; } + // ignore 404 and return an empty object + if (ignore404Errors && SavedObjectsErrorHelpers.isNotFoundError(error)) { + return {}; + } + if (this.isIgnorableError(error, ignore401Errors)) { return {}; } diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts index d48dbe9da2d3..14e7aa6430da 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts @@ -54,7 +54,7 @@ describe('UserUISettingsClientWrapper', () => { it('should replace user id placeholder with real user id', async () => { await wrapperClient.get('config', `${CURRENT_USER}_3.0.0`); - expect(mockedClient.get).toBeCalledWith('config', 'test_user_3.0.0', {}); + expect(mockedClient.get).toBeCalledWith('config', 'test_user', {}); }); }); @@ -80,7 +80,7 @@ describe('UserUISettingsClientWrapper', () => { it('should replace user id placeholder with real user id', async () => { await wrapperClient.update('config', `${CURRENT_USER}_3.0.0`, {}); - expect(mockedClient.update).toBeCalledWith('config', 'test_user_3.0.0', {}, {}); + expect(mockedClient.update).toBeCalledWith('config', 'test_user', {}, {}); }); }); @@ -110,7 +110,7 @@ describe('UserUISettingsClientWrapper', () => { 'config', {}, { - id: 'test_user_3.0.0', + id: 'test_user', references: [ { id: 'test_user', @@ -130,7 +130,7 @@ describe('UserUISettingsClientWrapper', () => { 'config', {}, { - id: 'test_user_3.0.0', + id: 'test_user', references: [ { id: 'test_user', @@ -226,3 +226,60 @@ describe('UserUISettingsClientWrapper', () => { }); }); }); + +describe('UserUISettingsClientWrapper - security not enabled', () => { + // security not enabled + beforeEach(() => { + jest.mock('../utils'); + }); + + const requestHandlerContext = coreMock.createRequestHandlerContext(); + const mockedClient = savedObjectsClientMock.create(); + const requestMock = httpServerMock.createOpenSearchDashboardsRequest(); + + const buildWrapperInstance = (permissionEnabled: boolean) => { + const wrapperInstance = new UserUISettingsClientWrapper(loggerMock.create(), permissionEnabled); + const wrapperClient = wrapperInstance.wrapperFactory({ + client: mockedClient, + typeRegistry: requestHandlerContext.savedObjects.typeRegistry, + request: requestMock, + }); + return wrapperClient; + }; + + const wrapperClient = buildWrapperInstance(false); + + describe('#get', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should replace user id placeholder with version', async () => { + await wrapperClient.get('config', `${CURRENT_USER}_3.0.0`); + + expect(mockedClient.get).toBeCalledWith('config', '3.0.0', {}); + }); + }); + + describe('#update', () => { + it('should replace user id placeholder with version', async () => { + await wrapperClient.update('config', `${CURRENT_USER}_3.0.0`, {}); + + expect(mockedClient.update).toBeCalledWith('config', '3.0.0', {}, {}); + }); + }); + + describe('#create', () => { + it('should replace user id placeholder with version', async () => { + await wrapperClient.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + + expect(mockedClient.create).toBeCalledWith( + 'config', + {}, + { + id: '3.0.0', + } + ); + }); + }); +}); diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index c7c3fa74bce8..d6a411f315d1 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -42,8 +42,10 @@ export class UserUISettingsClientWrapper { const userName = extractUserName(request, core); if (this.isUserLevelSetting(id)) { if (userName) { - return id.replace(CURRENT_USER, userName); + // return id.replace(CURRENT_USER, userName); // uncomment this to support version for user personal setting + return userName; } else { + // security is not enabled, using global setting id return id.replace(`${CURRENT_USER}_`, ''); } } @@ -102,8 +104,11 @@ export class UserUISettingsClientWrapper { .getPermissions()!, }; + // remove buildNum from attributes + const { buildNum, ...others } = attributes as any; + // create with reference, the reference field will used for filter settings by user - return await wrapperOptions.client.create(type, attributes, { + return await wrapperOptions.client.create(type, others, { ...options, id: docId, references: [ From a5156dce887664ad8e9e5f5c825b7688526e9ddc Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 4 Sep 2024 00:08:55 +0800 Subject: [PATCH 11/21] clean up Signed-off-by: Hailong Cui --- src/core/server/ui_settings/ui_settings_client.ts | 7 +------ .../fixtures/opensearch_archiver/visualize/data.json | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 43194fcf6778..80c49869f245 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -36,7 +36,6 @@ import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; import { CannotOverrideError } from './ui_settings_errors'; -import { HttpServiceStart } from '..'; export interface UiSettingsServiceOptions { type: string; @@ -46,7 +45,6 @@ export interface UiSettingsServiceOptions { overrides?: Record; defaults?: Record; log: Logger; - httpStart?: HttpServiceStart; } interface ReadOptions { @@ -297,11 +295,8 @@ export class UiSettingsClient implements IUiSettingsClient { userLevel = false, ignore404Errors = false, }: ReadOptions = {}): Promise> { - let docId = this.id; - if (userLevel) { - docId = `${CURRENT_USER}_${this.id}`; - } try { + const docId = userLevel ? `${CURRENT_USER}_${this.id}` : this.id; const resp = await this.savedObjectsClient.get>(this.type, docId); return this.translateChanges(resp.attributes, 'timelion', 'timeline'); } catch (error) { diff --git a/test/functional/fixtures/opensearch_archiver/visualize/data.json b/test/functional/fixtures/opensearch_archiver/visualize/data.json index f9250c37f61a..b3245fb765e6 100644 --- a/test/functional/fixtures/opensearch_archiver/visualize/data.json +++ b/test/functional/fixtures/opensearch_archiver/visualize/data.json @@ -206,7 +206,7 @@ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", "title": "test_index*" }, - "type": "index-pattern" + "type": "test_index*" } } } From cf755c85457be345a8695e97fc2224a2b4c17e87 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 4 Sep 2024 11:18:45 +0800 Subject: [PATCH 12/21] add customized render for permissions column Signed-off-by: Hailong Cui --- .../default_workspace.test.tsx.snap | 42 +++++++++++++------ .../workspace_list/default_workspace.test.tsx | 8 ++-- .../components/workspace_list/index.tsx | 14 +++++-- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap index dd1be51cf57f..0bb3f51ead3d 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap @@ -286,11 +286,17 @@ exports[`UserDefaultWorkspace should render title and table normally 1`] = ` Permissions
+ class="euiToolTipAnchor" + > +
+ Admin +
+
- Set as default + Set as my default
@@ -448,11 +454,17 @@ exports[`UserDefaultWorkspace should render title and table normally 1`] = ` Permissions
+ class="euiToolTipAnchor" + > +
+ Admin +
+
- Set as default + Set as my default
@@ -608,11 +620,17 @@ exports[`UserDefaultWorkspace should render title and table normally 1`] = ` Permissions
+ class="euiToolTipAnchor" + > +
+ Admin +
+
- Set as default + Set as my default
diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx index 20a2afda0216..f6a50b1c1350 100644 --- a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx @@ -4,9 +4,8 @@ */ import React from 'react'; -import moment from 'moment'; -import { BehaviorSubject, of } from 'rxjs'; -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import { BehaviorSubject } from 'rxjs'; +import { render } from '@testing-library/react'; import { I18nProvider } from '@osd/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; import { createMockedRegisteredUseCases$ } from '../../mocks'; @@ -148,7 +147,6 @@ describe('UserDefaultWorkspace', () => { expect(document.querySelectorAll('.euiTableRow-isSelectable').length).toBe(0); // action button Set as default in document - expect(queryAllByText('Set as default')).toHaveLength(3); - expect(queryAllByText('Copy ID')).toHaveLength(3); + expect(queryAllByText('Set as my default')).toHaveLength(3); }); }); diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 12dfce58c47d..b0e7a6d635af 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -29,6 +29,7 @@ import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, of } from 'rxjs'; import { i18n } from '@osd/i18n'; import { isString } from 'lodash'; +import { startCase } from 'lodash'; import { DEFAULT_NAV_GROUPS, WorkspaceAttribute, @@ -414,6 +415,15 @@ export const WorkspaceListInner = ({ field: 'permissionMode', name: 'Permissions', width: '6%', + render: (permissionMode: WorkspaceAttributeWithPermission['permissionMode']) => { + return isDashboardAdmin ? ( + + Admin + + ) : ( + startCase(permissionMode) + ); + }, }, { field: 'lastUpdatedTime', @@ -451,7 +461,7 @@ export const WorkspaceListInner = ({ onClick: ({ id }: WorkspaceAttribute) => handleSwitchWorkspace(id), }, { - name: Set as default, + name: Set as my default, icon: 'flag', type: 'icon', description: 'Set as default workspace', @@ -464,8 +474,6 @@ export const WorkspaceListInner = ({ type: 'icon', description: 'Leave workspace', 'data-test-subj': 'workspace-list-leave-icon', - enabled: (item: WorkspaceAttributeWithPermission) => - item.permissionMode === PermissionModeId.Owner, available: () => false, }, { From f40d9b5554578cec910e9bd372a2e0c0d44b5c62 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 5 Sep 2024 17:08:06 +0800 Subject: [PATCH 13/21] address review comments Signed-off-by: Hailong Cui --- src/core/server/index.ts | 3 +- .../create_or_upgrade_saved_config.test.ts | 2 +- .../create_or_upgrade_saved_config.ts | 13 +- .../get_upgradeable_config.ts | 11 +- src/core/server/ui_settings/index.ts | 7 +- src/core/server/ui_settings/routes/delete.ts | 10 +- src/core/server/ui_settings/routes/get.ts | 16 +- src/core/server/ui_settings/routes/set.ts | 10 +- .../server/ui_settings/routes/set_many.ts | 10 +- src/core/server/ui_settings/types.ts | 24 ++- .../ui_settings/ui_settings_client.test.ts | 29 +++- .../server/ui_settings/ui_settings_client.ts | 156 ++++++++++++------ src/core/types/ui_settings.ts | 11 +- .../opensearch_dashboards.json | 4 +- .../advanced_settings/public/plugin.ts | 15 +- src/plugins/advanced_settings/public/types.ts | 2 +- .../advanced_settings/server/plugin.ts | 2 +- .../user_ui_settings_client_wrapper.test.ts | 43 +++-- .../user_ui_settings_client_wrapper.ts | 8 +- .../components/workspace_list/index.tsx | 15 +- 20 files changed, 258 insertions(+), 133 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 14d7b93c5f68..c697e15b9777 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -358,7 +358,8 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, - CURRENT_USER, + CURRENT_USER_PLACEHOLDER, + UiSettingScope, } from './ui_settings'; export { diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 47491488f980..bbfb04cff90a 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -155,7 +155,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { Object { "newVersion": "4.0.1", "prevVersion": "4.0.0", - "userLevel": false, + "scope": undefined, }, ], ] diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index bf64fa909829..2521e3af1bcb 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -35,7 +35,8 @@ import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; -import { CURRENT_USER } from '../ui_settings_client'; +import { generateDocId } from '../ui_settings_client'; +import { UiSettingScope } from '../types'; interface Options { savedObjectsClient: SavedObjectsClientContract; @@ -43,19 +44,19 @@ interface Options { buildNum: number; log: Logger; handleWriteErrors: boolean; - userLevel?: boolean; + scope?: UiSettingScope; } export async function createOrUpgradeSavedConfig( options: Options ): Promise | undefined> { - const { savedObjectsClient, version, buildNum, log, handleWriteErrors, userLevel } = options; + const { savedObjectsClient, version, buildNum, log, handleWriteErrors, scope } = options; // try to find an older config we can upgrade const upgradeableConfig = await getUpgradeableConfig({ savedObjectsClient, version, - userLevel, + scope, }); // default to the attributes of the upgradeableConfig if available @@ -65,7 +66,7 @@ export async function createOrUpgradeSavedConfig( ); try { - const docId = userLevel ? `${CURRENT_USER}_${version}` : version; + const docId = generateDocId(version, scope); // create the new SavedConfig await savedObjectsClient.create('config', attributes, { id: docId }); } catch (error) { @@ -89,7 +90,7 @@ export async function createOrUpgradeSavedConfig( log.debug(`Upgrade config from ${upgradeableConfig.id} to ${version}`, { prevVersion: upgradeableConfig.id, newVersion: version, - userLevel: !!userLevel, + scope, }); } } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 416233c8ff4e..d0859ba5ce32 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -29,7 +29,8 @@ */ import { SavedObjectsClientContract } from '../../saved_objects/types'; -import { CURRENT_USER } from '../ui_settings_client'; +import { UiSettingScope } from '../types'; +import { CURRENT_USER_PLACEHOLDER } from '../ui_settings_client'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; /** @@ -42,11 +43,11 @@ import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; export async function getUpgradeableConfig({ savedObjectsClient, version, - userLevel, + scope, }: { savedObjectsClient: SavedObjectsClientContract; version: string; - userLevel?: boolean; + scope?: UiSettingScope; }) { // attempt to find a config we can upgrade const { saved_objects: savedConfigs } = await savedObjectsClient.find({ @@ -55,7 +56,9 @@ export async function getUpgradeableConfig({ perPage: 1000, sortField: 'buildNum', sortOrder: 'desc', - ...(userLevel ? { hasReference: { type: 'user', id: CURRENT_USER } } : {}), + ...(scope && scope === UiSettingScope.USER + ? { hasReference: { type: 'user', id: CURRENT_USER_PLACEHOLDER } } + : {}), }); // try to find a config that we can upgrade diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index a3cfa84f2ac7..0dda84023a5a 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -28,7 +28,11 @@ * under the License. */ -export { UiSettingsClient, UiSettingsServiceOptions, CURRENT_USER } from './ui_settings_client'; +export { + UiSettingsClient, + UiSettingsServiceOptions, + CURRENT_USER_PLACEHOLDER, +} from './ui_settings_client'; export { config } from './ui_settings_config'; export { UiSettingsService } from './ui_settings_service'; @@ -48,4 +52,5 @@ export { StringValidation, StringValidationRegex, StringValidationRegexString, + UiSettingScope, } from './types'; diff --git a/src/core/server/ui_settings/routes/delete.ts b/src/core/server/ui_settings/routes/delete.ts index d42eb948259e..22d01159c4ac 100644 --- a/src/core/server/ui_settings/routes/delete.ts +++ b/src/core/server/ui_settings/routes/delete.ts @@ -33,11 +33,17 @@ import { schema } from '@osd/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { CannotOverrideError } from '../ui_settings_errors'; +import { UiSettingScope } from '../types'; const validate = { params: schema.object({ key: schema.string(), }), + body: schema.object({ + scope: schema.maybe( + schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) + ), + }), }; export function registerDeleteRoute(router: IRouter) { @@ -47,7 +53,9 @@ export function registerDeleteRoute(router: IRouter) { try { const uiSettingsClient = context.core.uiSettings.client; - await uiSettingsClient.remove(request.params.key); + const { scope } = request.body; + + await uiSettingsClient.remove(request.params.key, scope); return response.ok({ body: { diff --git a/src/core/server/ui_settings/routes/get.ts b/src/core/server/ui_settings/routes/get.ts index 000d16a37358..ea7d1d555ca3 100644 --- a/src/core/server/ui_settings/routes/get.ts +++ b/src/core/server/ui_settings/routes/get.ts @@ -28,18 +28,30 @@ * under the License. */ +import { schema } from '@osd/config-schema'; + import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; +import { UiSettingScope } from '../types'; + +const validate = { + body: schema.object({ + scope: schema.maybe( + schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) + ), + }), +}; export function registerGetRoute(router: IRouter) { router.get( - { path: '/api/opensearch-dashboards/settings', validate: false }, + { path: '/api/opensearch-dashboards/settings', validate }, async (context, request, response) => { try { const uiSettingsClient = context.core.uiSettings.client; + const { scope } = request.body; return response.ok({ body: { - settings: await uiSettingsClient.getUserProvided(), + settings: await uiSettingsClient.getUserProvided(scope), }, }); } catch (error) { diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts index d30b7d705d6a..3beba658a332 100644 --- a/src/core/server/ui_settings/routes/set.ts +++ b/src/core/server/ui_settings/routes/set.ts @@ -33,6 +33,7 @@ import { schema, ValidationError } from '@osd/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { CannotOverrideError } from '../ui_settings_errors'; +import { UiSettingScope } from '../types'; const validate = { params: schema.object({ @@ -40,6 +41,9 @@ const validate = { }), body: schema.object({ value: schema.any(), + scope: schema.maybe( + schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) + ), }), }; @@ -51,13 +55,13 @@ export function registerSetRoute(router: IRouter) { const uiSettingsClient = context.core.uiSettings.client; const { key } = request.params; - const { value } = request.body; + const { value, scope } = request.body; - await uiSettingsClient.set(key, value); + await uiSettingsClient.set(key, value, scope); return response.ok({ body: { - settings: await uiSettingsClient.getUserProvided(), + settings: await uiSettingsClient.getUserProvided(scope), }, }); } catch (error) { diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 8698445a2c5b..5100f2c6577d 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -33,10 +33,14 @@ import { schema, ValidationError } from '@osd/config-schema'; import { IRouter } from '../../http'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { CannotOverrideError } from '../ui_settings_errors'; +import { UiSettingScope } from '../types'; const validate = { body: schema.object({ changes: schema.object({}, { unknowns: 'allow' }), + scope: schema.maybe( + schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) + ), }), }; @@ -47,13 +51,13 @@ export function registerSetManyRoute(router: IRouter) { try { const uiSettingsClient = context.core.uiSettings.client; - const { changes } = request.body; + const { changes, scope } = request.body; - await uiSettingsClient.setMany(changes); + await uiSettingsClient.setMany(changes, scope); return response.ok({ body: { - settings: await uiSettingsClient.getUserProvided(), + settings: await uiSettingsClient.getUserProvided(scope), }, }); } catch (error) { diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index 9602c2a42a91..91771b1bf829 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -29,7 +29,12 @@ */ import { SavedObjectsClientContract } from '../saved_objects/types'; -import { UiSettingsParams, UserProvidedValues, PublicUiSettingsParams } from '../../types'; +import { + UiSettingsParams, + UserProvidedValues, + PublicUiSettingsParams, + UiSettingScope, +} from '../../types'; export { UiSettingsParams, PublicUiSettingsParams, @@ -40,6 +45,7 @@ export { ImageValidation, UiSettingsType, UserProvidedValues, + UiSettingScope, } from '../../types'; /** @@ -66,31 +72,33 @@ export interface IUiSettingsClient { /** * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. */ - get: (key: string) => Promise; + get: (key: string, scope?: UiSettingScope) => Promise; /** * Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. */ - getAll: () => Promise>; + getAll: (scope?: UiSettingScope) => Promise>; /** * Retrieves a set of all uiSettings values set by the user. */ - getUserProvided: () => Promise>>; + getUserProvided: ( + scope?: UiSettingScope + ) => Promise>>; /** * Writes multiple uiSettings values and marks them as set by the user. */ - setMany: (changes: Record) => Promise; + setMany: (changes: Record, scope?: UiSettingScope) => Promise; /** * Writes uiSettings value and marks it as set by the user. */ - set: (key: string, value: any) => Promise; + set: (key: string, value: any, scope?: UiSettingScope) => Promise; /** * Removes uiSettings value by key. */ - remove: (key: string) => Promise; + remove: (key: string, scope?: UiSettingScope) => Promise; /** * Removes multiple uiSettings values by keys. */ - removeMany: (keys: string[]) => Promise; + removeMany: (keys: string[], scope?: UiSettingScope) => Promise; /** * Shows whether the uiSettings value set by the user. */ diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 55d828bb1c53..628ff3f8bc93 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -36,7 +36,7 @@ import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config import { SavedObjectsClient } from '../saved_objects'; import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; -import { CURRENT_USER, UiSettingsClient } from './ui_settings_client'; +import { CURRENT_USER_PLACEHOLDER, UiSettingsClient } from './ui_settings_client'; import { CannotOverrideError } from './ui_settings_errors'; const logger = loggingSystemMock.create().get(); @@ -116,9 +116,13 @@ describe('ui settings', () => { one: 'value', another: 'val', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`, { - key: 'value', - }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + TYPE, + `${CURRENT_USER_PLACEHOLDER}_${ID}`, + { + key: 'value', + } + ); }); it('automatically creates the savedConfig if it is missing', async () => { @@ -375,7 +379,10 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); - expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); + expect(savedObjectsClient.get).toHaveBeenCalledWith( + TYPE, + `${CURRENT_USER_PLACEHOLDER}_${ID}` + ); }); it('returns user configuration', async () => { @@ -542,7 +549,10 @@ describe('ui settings', () => { await uiSettings.getAll(); expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); - expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); + expect(savedObjectsClient.get).toHaveBeenCalledWith( + TYPE, + `${CURRENT_USER_PLACEHOLDER}_${ID}` + ); }); it('returns defaults when opensearch doc is empty', async () => { @@ -641,7 +651,7 @@ describe('ui settings', () => { const { uiSettings, savedObjectsClient } = setup({ defaults }); savedObjectsClient.get.mockImplementation((_type, id, _options) => { - if (id === `${CURRENT_USER}_${ID}`) { + if (id === `${CURRENT_USER_PLACEHOLDER}_${ID}`) { return Promise.resolve({ attributes: { bar: 'my personal value', @@ -691,7 +701,10 @@ describe('ui settings', () => { expect(savedObjectsClient.get).toHaveBeenCalledTimes(2); expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, ID); - expect(savedObjectsClient.get).toHaveBeenCalledWith(TYPE, `${CURRENT_USER}_${ID}`); + expect(savedObjectsClient.get).toHaveBeenCalledWith( + TYPE, + `${CURRENT_USER_PLACEHOLDER}_${ID}` + ); }); it(`returns the promised value for a key`, async () => { diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 80c49869f245..1592c29bb6dd 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -34,7 +34,12 @@ import { SavedObjectsErrorHelpers } from '../saved_objects'; import { SavedObjectsClientContract } from '../saved_objects/types'; import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; -import { IUiSettingsClient, UiSettingsParams, PublicUiSettingsParams } from './types'; +import { + IUiSettingsClient, + UiSettingsParams, + PublicUiSettingsParams, + UiSettingScope, +} from './types'; import { CannotOverrideError } from './ui_settings_errors'; export interface UiSettingsServiceOptions { @@ -50,7 +55,7 @@ export interface UiSettingsServiceOptions { interface ReadOptions { ignore401Errors?: boolean; autoCreateOrUpgradeIfMissing?: boolean; - userLevel?: boolean; + scope?: UiSettingScope; ignore404Errors?: boolean; } @@ -65,7 +70,32 @@ type UserProvided = Record>; type UiSettingsRaw = Record; // identifier for current user -export const CURRENT_USER = ''; +export const CURRENT_USER_PLACEHOLDER = ''; + +/** + * default scope read options, order matters + */ +const UiSettingScopeReadOptions = [ + { + scope: UiSettingScope.GLOBAL, + ignore401Errors: false, + autoCreateOrUpgradeIfMissing: true, + ignore404Errors: false, + }, + { + scope: UiSettingScope.USER, + ignore401Errors: true, + autoCreateOrUpgradeIfMissing: false, + ignore404Errors: true, + }, +] as ReadOptions[]; + +export const generateDocId = (id: string, scope?: UiSettingScope) => { + if (scope === UiSettingScope.USER) { + return `${CURRENT_USER_PLACEHOLDER}_${id}`; + } + return id; +}; export class UiSettingsClient implements IUiSettingsClient { private readonly type: UiSettingsServiceOptions['type']; @@ -105,13 +135,13 @@ export class UiSettingsClient implements IUiSettingsClient { return this.defaults[key]?.value; } - async get(key: string): Promise { - const all = await this.getAll(); + async get(key: string, scope?: UiSettingScope): Promise { + const all = await this.getAll(scope); return all[key]; } - async getAll() { - const raw = await this.getRaw(); + async getAll(scope?: UiSettingScope) { + const raw = await this.getRaw(scope); return Object.keys(raw).reduce((all, key) => { const item = raw[key]; @@ -120,60 +150,60 @@ export class UiSettingsClient implements IUiSettingsClient { }, {} as Record); } - async getUserProvided(): Promise> { - // user provided for global - const userProvided: UserProvided = this.onReadHook(await this.read()); - /** - * user personal settings, which is not versioned, so that we don't need to create when get - * and ignore 404 when not found the document - */ - const userLevelProvided: UserProvided = this.onReadHook( - await this.read({ - userLevel: true, - autoCreateOrUpgradeIfMissing: false, - ignore404Errors: true, - }) - ); + async getUserProvided(scope?: UiSettingScope): Promise> { + let userProvided: UserProvided = {}; + if (scope) { + const readOptions = UiSettingScopeReadOptions.find((option) => option.scope === scope); + userProvided = this.onReadHook(await this.read(readOptions)); + } else { + // default will get from all scope and merge + // loop UiSettingScopeReadOptions + for (const readOptions of UiSettingScopeReadOptions) { + userProvided = { ...userProvided, ...this.onReadHook(await this.read(readOptions)) }; + } + } // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object for (const [key, value] of Object.entries(this.overrides)) { userProvided[key] = value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; - - userLevelProvided[key] = - value === null ? { isOverridden: true } : { isOverridden: true, userValue: value }; } - return { ...userProvided, ...userLevelProvided }; + return userProvided; } - async setMany(changes: Record) { - this.onWriteHook(changes); - // group changes into by different scope - const [global, personal] = this.groupChanges(changes); - if (global && Object.keys(global).length > 0) { - await this.write({ changes: global }); - } - if (personal && Object.keys(personal).length > 0) { - await this.write({ changes: personal, userLevel: true }); + async setMany(changes: Record, scope?: UiSettingScope) { + this.onWriteHook(changes, scope); + + if (scope) { + await this.write({ changes, scope }); + } else { + // group changes into by different scope + const [global, personal] = this.groupChanges(changes); + if (global && Object.keys(global).length > 0) { + await this.write({ changes: global }); + } + if (personal && Object.keys(personal).length > 0) { + await this.write({ changes: personal, scope: UiSettingScope.USER }); + } } } - async set(key: string, value: any) { - await this.setMany({ [key]: value }); + async set(key: string, value: any, scope?: UiSettingScope) { + await this.setMany({ [key]: value }, scope); } - async remove(key: string) { - await this.set(key, null); + async remove(key: string, scope?: UiSettingScope) { + await this.set(key, null, scope); } - async removeMany(keys: string[]) { + async removeMany(keys: string[], scope?: UiSettingScope) { const changes: Record = {}; keys.forEach((key) => { changes[key] = null; }); - await this.setMany(changes); + await this.setMany(changes, scope); } isOverridden(key: string) { @@ -186,8 +216,8 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private async getRaw(): Promise { - const userProvided = await this.getUserProvided(); + private async getRaw(scope?: UiSettingScope): Promise { + const userProvided = await this.getUserProvided(scope); return defaultsDeep(userProvided, this.defaults); } @@ -199,7 +229,20 @@ export class UiSettingsClient implements IUiSettingsClient { } } - private onWriteHook(changes: Record) { + private validateScope(key: string, value: unknown, scope?: UiSettingScope) { + const definition = this.defaults[key]; + if (value === null || definition === undefined) return; + const validScopes = Array.isArray(definition.scope) + ? definition.scope + : [definition.scope || UiSettingScope.GLOBAL]; + if (scope && !validScopes.includes(scope)) { + throw new Error( + `Unable to update "${key}" with "${scope}" because the valid scopes are "${validScopes}"` + ); + } + } + + private onWriteHook(changes: Record, scope?: UiSettingScope) { for (const key of Object.keys(changes)) { this.assertUpdateAllowed(key); } @@ -207,6 +250,10 @@ export class UiSettingsClient implements IUiSettingsClient { for (const [key, value] of Object.entries(changes)) { this.validateKey(key, value); } + + for (const [key, value] of Object.entries(changes)) { + this.validateScope(key, value, scope); + } } private onReadHook(values: Record) { @@ -236,7 +283,10 @@ export class UiSettingsClient implements IUiSettingsClient { private groupChanges(changes: Record) { const userLevelKeys = [] as string[]; Object.entries(this.defaults).forEach(([key, value]) => { - if (value.scope === 'user' || (Array.isArray(value.scope) && value.scope.includes('user'))) { + if ( + value.scope === UiSettingScope.USER || + (Array.isArray(value.scope) && value.scope.includes(UiSettingScope.USER)) + ) { userLevelKeys.push(key); } }); @@ -257,15 +307,15 @@ export class UiSettingsClient implements IUiSettingsClient { private async write({ changes, autoCreateOrUpgradeIfMissing = true, - userLevel = false, + scope, }: { changes: Record; autoCreateOrUpgradeIfMissing?: boolean; - userLevel?: boolean; + scope?: UiSettingScope; }) { changes = this.translateChanges(changes, 'timeline', 'timelion'); try { - const docId = userLevel ? `${CURRENT_USER}_${this.id}` : this.id; + const docId = generateDocId(this.id, scope); await this.savedObjectsClient.update(this.type, docId, changes); } catch (error) { if (!SavedObjectsErrorHelpers.isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) { @@ -278,13 +328,13 @@ export class UiSettingsClient implements IUiSettingsClient { buildNum: this.buildNum, log: this.log, handleWriteErrors: false, - userLevel, + scope, }); await this.write({ changes, autoCreateOrUpgradeIfMissing: false, - userLevel, + scope, }); } } @@ -292,11 +342,11 @@ export class UiSettingsClient implements IUiSettingsClient { private async read({ ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, - userLevel = false, ignore404Errors = false, + scope, }: ReadOptions = {}): Promise> { try { - const docId = userLevel ? `${CURRENT_USER}_${this.id}` : this.id; + const docId = generateDocId(this.id, scope); const resp = await this.savedObjectsClient.get>(this.type, docId); return this.translateChanges(resp.attributes, 'timelion', 'timeline'); } catch (error) { @@ -307,14 +357,14 @@ export class UiSettingsClient implements IUiSettingsClient { buildNum: this.buildNum, log: this.log, handleWriteErrors: true, - userLevel, + scope, }); if (!failedUpgradeAttributes) { return await this.read({ ignore401Errors, autoCreateOrUpgradeIfMissing: false, - userLevel, + scope, }); } diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index e8c6b9130b9e..58297589cd54 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -56,7 +56,14 @@ export interface DeprecationSettings { docLinksKey: string; } -export type SettingScope = 'global' | 'user'; +/** + * UiSettings scope options. + * @public + */ +export enum UiSettingScope { + GLOBAL = 'global', + USER = 'user', +} /** * UiSettings parameters defined by the plugins. @@ -68,7 +75,7 @@ export interface UiSettingsParams { /** * scope of the setting item */ - scope?: SettingScope | SettingScope[]; + scope?: UiSettingScope | UiSettingScope[]; /** default value to fall back to if a user doesn't provide any */ value?: T; /** description provided to a user in UI */ diff --git a/src/plugins/advanced_settings/opensearch_dashboards.json b/src/plugins/advanced_settings/opensearch_dashboards.json index 30d5ac045c74..406fb402de79 100644 --- a/src/plugins/advanced_settings/opensearch_dashboards.json +++ b/src/plugins/advanced_settings/opensearch_dashboards.json @@ -3,7 +3,7 @@ "version": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["management","navigation"], - "optionalPlugins": ["home", "contentManagement"], + "requiredPlugins": ["management","navigation", "contentManagement"], + "optionalPlugins": ["home"], "requiredBundles": ["opensearchDashboardsReact", "home"] } diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 6ea960712046..6811290ac0f9 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -60,6 +60,7 @@ const titleInGroup = i18n.translate('advancedSettings.applicationSettingsLabel', defaultMessage: 'Application settings', }); +const USER_SETTINGS_APPID = 'user_settings'; export class AdvancedSettingsPlugin implements Plugin< @@ -129,14 +130,12 @@ export class AdvancedSettingsPlugin if (core.chrome.navGroup.getNavGroupEnabled()) { setupUserSettingsPage(contentManagementSetup); - core.application.registerAppUpdater(this.appUpdater$); - const userSettingTitle = i18n.translate('advancedSettings.userSettingsLabel', { defaultMessage: 'User settings', }); core.application.register({ - id: 'user_settings', + id: USER_SETTINGS_APPID, title: userSettingTitle, updater$: this.appUpdater$, navLinkStatus: core.chrome.navGroup.getNavGroupEnabled() @@ -158,8 +157,8 @@ export class AdvancedSettingsPlugin core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.settingsAndSetup, [ { - id: 'user_settings', - order: 101, + id: USER_SETTINGS_APPID, + order: 101, // just right after application settings which order is 100 }, ]); } @@ -191,10 +190,10 @@ export class AdvancedSettingsPlugin } this.appUpdater$.next((app) => { - const securityEnabled = core.application.capabilities.userSettings?.enabled; - if (app.id === 'user_settings') { + const userSettingsEnabled = core.application.capabilities.userSettings?.enabled; + if (app.id === USER_SETTINGS_APPID) { return { - navLinkStatus: securityEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + navLinkStatus: userSettingsEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, }; } }); diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index e7c349d1135a..435c1a3b5cee 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -48,7 +48,7 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; - contentManagement?: ContentManagementPluginSetup; + contentManagement: ContentManagementPluginSetup; } export interface AdvancedSettingsPluginStart { diff --git a/src/plugins/advanced_settings/server/plugin.ts b/src/plugins/advanced_settings/server/plugin.ts index a7b478920062..42f60d9eea97 100644 --- a/src/plugins/advanced_settings/server/plugin.ts +++ b/src/plugins/advanced_settings/server/plugin.ts @@ -75,7 +75,7 @@ export class AdvancedSettingsServerPlugin implements Plugin { ); this.userUiSettingsClientWrapper = userUiSettingsClientWrapper; core.savedObjects.addClientWrapper( - 3, + 3, // The wrapper should be triggered after workspace_id_consumer wrapper which id is -3 to avoid creating user settings within any workspace. 'user_ui_settings', userUiSettingsClientWrapper.wrapperFactory ); diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts index 14e7aa6430da..5f32fb200a2f 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts @@ -7,7 +7,7 @@ import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../co import { UserUISettingsClientWrapper } from './user_ui_settings_client_wrapper'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { loggerMock } from '../../../../core/server/logging/logger.mock'; -import { CURRENT_USER } from '../../../../core/server'; +import { CURRENT_USER_PLACEHOLDER } from '../../../../core/server'; jest.mock('../utils', () => { return { @@ -46,13 +46,13 @@ describe('UserUISettingsClientWrapper', () => { }); it('should skip replacing user id if type is not config', async () => { - await wrapperClient.get('config1', `${CURRENT_USER}_3.0.0`); + await wrapperClient.get('config1', `${CURRENT_USER_PLACEHOLDER}_3.0.0`); - expect(mockedClient.get).toBeCalledWith('config1', `${CURRENT_USER}_3.0.0`, {}); + expect(mockedClient.get).toBeCalledWith('config1', `${CURRENT_USER_PLACEHOLDER}_3.0.0`, {}); }); it('should replace user id placeholder with real user id', async () => { - await wrapperClient.get('config', `${CURRENT_USER}_3.0.0`); + await wrapperClient.get('config', `${CURRENT_USER_PLACEHOLDER}_3.0.0`); expect(mockedClient.get).toBeCalledWith('config', 'test_user', {}); }); @@ -72,13 +72,18 @@ describe('UserUISettingsClientWrapper', () => { }); it('should skip replacing user id if type is not config', async () => { - await wrapperClient.update('config1', `${CURRENT_USER}_3.0.0`, {}); + await wrapperClient.update('config1', `${CURRENT_USER_PLACEHOLDER}_3.0.0`, {}); - expect(mockedClient.update).toBeCalledWith('config1', `${CURRENT_USER}_3.0.0`, {}, {}); + expect(mockedClient.update).toBeCalledWith( + 'config1', + `${CURRENT_USER_PLACEHOLDER}_3.0.0`, + {}, + {} + ); }); it('should replace user id placeholder with real user id', async () => { - await wrapperClient.update('config', `${CURRENT_USER}_3.0.0`, {}); + await wrapperClient.update('config', `${CURRENT_USER_PLACEHOLDER}_3.0.0`, {}); expect(mockedClient.update).toBeCalledWith('config', 'test_user', {}, {}); }); @@ -98,13 +103,17 @@ describe('UserUISettingsClientWrapper', () => { }); it('should skip replacing user id if type is not config', async () => { - await wrapperClient.create('config1', {}, { id: `${CURRENT_USER}_3.0.0` }); + await wrapperClient.create('config1', {}, { id: `${CURRENT_USER_PLACEHOLDER}_3.0.0` }); - expect(mockedClient.create).toBeCalledWith('config1', {}, { id: `${CURRENT_USER}_3.0.0` }); + expect(mockedClient.create).toBeCalledWith( + 'config1', + {}, + { id: `${CURRENT_USER_PLACEHOLDER}_3.0.0` } + ); }); it('should replace user id placeholder with real user id', async () => { - await wrapperClient.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + await wrapperClient.create('config', {}, { id: `${CURRENT_USER_PLACEHOLDER}_3.0.0` }); expect(mockedClient.create).toBeCalledWith( 'config', @@ -124,7 +133,11 @@ describe('UserUISettingsClientWrapper', () => { it('should replace user id placeholder with real user id and permission enabled', async () => { const wrapperClientWithPermission = buildWrapperInstance(true); - await wrapperClientWithPermission.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + await wrapperClientWithPermission.create( + 'config', + {}, + { id: `${CURRENT_USER_PLACEHOLDER}_3.0.0` } + ); expect(mockedClient.create).toBeCalledWith( 'config', @@ -180,7 +193,7 @@ describe('UserUISettingsClientWrapper', () => { type: 'config', hasReference: { type: 'user', - id: CURRENT_USER, + id: CURRENT_USER_PLACEHOLDER, }, }); @@ -255,7 +268,7 @@ describe('UserUISettingsClientWrapper - security not enabled', () => { }); it('should replace user id placeholder with version', async () => { - await wrapperClient.get('config', `${CURRENT_USER}_3.0.0`); + await wrapperClient.get('config', `${CURRENT_USER_PLACEHOLDER}_3.0.0`); expect(mockedClient.get).toBeCalledWith('config', '3.0.0', {}); }); @@ -263,7 +276,7 @@ describe('UserUISettingsClientWrapper - security not enabled', () => { describe('#update', () => { it('should replace user id placeholder with version', async () => { - await wrapperClient.update('config', `${CURRENT_USER}_3.0.0`, {}); + await wrapperClient.update('config', `${CURRENT_USER_PLACEHOLDER}_3.0.0`, {}); expect(mockedClient.update).toBeCalledWith('config', '3.0.0', {}, {}); }); @@ -271,7 +284,7 @@ describe('UserUISettingsClientWrapper - security not enabled', () => { describe('#create', () => { it('should replace user id placeholder with version', async () => { - await wrapperClient.create('config', {}, { id: `${CURRENT_USER}_3.0.0` }); + await wrapperClient.create('config', {}, { id: `${CURRENT_USER_PLACEHOLDER}_3.0.0` }); expect(mockedClient.create).toBeCalledWith( 'config', diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index d6a411f315d1..90bdbe321729 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -16,7 +16,7 @@ import { SavedObjectsFindResponse, OpenSearchDashboardsRequest, } from '../../../../core/server'; -import { Logger, CURRENT_USER } from '../../../../core/server'; +import { Logger, CURRENT_USER_PLACEHOLDER } from '../../../../core/server'; import { extractUserName } from '../utils'; /** @@ -35,7 +35,7 @@ export class UserUISettingsClientWrapper { } private isUserLevelSetting(id: string | undefined): boolean { - return id ? id.startsWith(CURRENT_USER) : false; + return id ? id.startsWith(CURRENT_USER_PLACEHOLDER) : false; } private normalizeDocId(id: string, request: OpenSearchDashboardsRequest, core?: CoreStart) { @@ -46,7 +46,7 @@ export class UserUISettingsClientWrapper { return userName; } else { // security is not enabled, using global setting id - return id.replace(`${CURRENT_USER}_`, ''); + return id.replace(`${CURRENT_USER_PLACEHOLDER}_`, ''); } } return id; @@ -137,7 +137,7 @@ export class UserUISettingsClientWrapper { const userName = extractUserName(wrapperOptions.request, this.core); const { hasReference } = options || {}; if (options.type === 'config' && userName && hasReference) { - const id = hasReference.id.replace(CURRENT_USER, userName); + const id = hasReference.id.replace(CURRENT_USER_PLACEHOLDER, userName); const resp: SavedObjectsFindResponse = await wrapperOptions.client.find({ ...options, hasReference: { ...hasReference, id }, diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index a54f741afa8f..1910c4b373c6 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -111,7 +111,7 @@ export const WorkspaceListInner = ({ const [selection, setSelection] = useState([]); const [allDataSources, setAllDataSources] = useState([]); // default workspace state - const [defaultWorkspace, setDefaultWorkspace] = useState(undefined); + const [defaultWorkspaceId, setDefaultWorkspaceId] = useState(undefined); const dateFormat = uiSettings?.get('dateFormat'); @@ -133,7 +133,7 @@ export const WorkspaceListInner = ({ ); useEffect(() => { - setDefaultWorkspace(uiSettings?.get(DEFAULT_WORKSPACE)); + setDefaultWorkspaceId(uiSettings?.get(DEFAULT_WORKSPACE)); if (savedObjects) { getDataSourcesList(savedObjects.client, ['*']).then((data) => { setAllDataSources(data); @@ -238,15 +238,12 @@ export const WorkspaceListInner = ({ async (item: WorkspaceAttribute) => { const set = await uiSettings?.set(DEFAULT_WORKSPACE, item.id); if (set) { - setDefaultWorkspace(item.id); + setDefaultWorkspaceId(item.id); notifications?.toasts.addSuccess(`Default workspace been set to ${item.name}`); } else { // toast - notifications?.toasts.addError( - new Error(`Failed to set workspace ${item.name} as default workspace.`), - { - title: 'Set default workspace error', - } + notifications?.toasts.addWarning( + `Failed to set workspace ${item.name} as default workspace.` ); } }, @@ -362,7 +359,7 @@ export const WorkspaceListInner = ({ {name} - {item.id === defaultWorkspace && ( + {item.id === defaultWorkspaceId && ( Default workspace From 9d2cc6e83794bbffe64931a5e03cd0c0a0bf264b Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 5 Sep 2024 19:27:15 +0800 Subject: [PATCH 14/21] address review comment Signed-off-by: Hailong Cui --- src/core/public/index.ts | 1 + .../create_or_upgrade_saved_config.ts | 2 +- .../get_upgradeable_config.ts | 2 +- src/core/server/ui_settings/index.ts | 8 ++-- .../ui_settings/ui_settings_client.test.ts | 3 +- .../server/ui_settings/ui_settings_client.ts | 13 ++---- src/core/server/ui_settings/utils.ts | 15 +++++++ src/core/server/utils/auth_info.ts | 45 +++++++++++++++++++ src/core/server/utils/index.ts | 1 + .../management_app/advanced_settings.tsx | 3 +- src/plugins/advanced_settings/server/utils.ts | 21 ++++----- 11 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 src/core/server/ui_settings/utils.ts create mode 100644 src/core/server/utils/auth_info.ts diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 2398b16e7c03..b813e12c97bf 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -131,6 +131,7 @@ export { NavGroupType, NavGroupStatus, WorkspaceAttributeWithPermission, + UiSettingScope, } from '../types'; export { diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 2521e3af1bcb..f4ba7d5176ef 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -35,8 +35,8 @@ import { SavedObjectsErrorHelpers } from '../../saved_objects/'; import { Logger } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; -import { generateDocId } from '../ui_settings_client'; import { UiSettingScope } from '../types'; +import { generateDocId } from '../utils'; interface Options { savedObjectsClient: SavedObjectsClientContract; diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index d0859ba5ce32..3d248e275f6d 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -30,7 +30,7 @@ import { SavedObjectsClientContract } from '../../saved_objects/types'; import { UiSettingScope } from '../types'; -import { CURRENT_USER_PLACEHOLDER } from '../ui_settings_client'; +import { CURRENT_USER_PLACEHOLDER } from '../utils'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; /** diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index 0dda84023a5a..0f98342125b5 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -28,11 +28,7 @@ * under the License. */ -export { - UiSettingsClient, - UiSettingsServiceOptions, - CURRENT_USER_PLACEHOLDER, -} from './ui_settings_client'; +export { UiSettingsClient, UiSettingsServiceOptions } from './ui_settings_client'; export { config } from './ui_settings_config'; export { UiSettingsService } from './ui_settings_service'; @@ -54,3 +50,5 @@ export { StringValidationRegexString, UiSettingScope, } from './types'; + +export { CURRENT_USER_PLACEHOLDER } from './utils'; diff --git a/src/core/server/ui_settings/ui_settings_client.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts index 628ff3f8bc93..4e4731690d05 100644 --- a/src/core/server/ui_settings/ui_settings_client.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -36,8 +36,9 @@ import { createOrUpgradeSavedConfigMock } from './create_or_upgrade_saved_config import { SavedObjectsClient } from '../saved_objects'; import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; -import { CURRENT_USER_PLACEHOLDER, UiSettingsClient } from './ui_settings_client'; +import { UiSettingsClient } from './ui_settings_client'; import { CannotOverrideError } from './ui_settings_errors'; +import { CURRENT_USER_PLACEHOLDER } from './utils'; const logger = loggingSystemMock.create().get(); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 1592c29bb6dd..0c4f8f0e74f4 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -41,6 +41,7 @@ import { UiSettingScope, } from './types'; import { CannotOverrideError } from './ui_settings_errors'; +import { generateDocId } from './utils'; export interface UiSettingsServiceOptions { type: string; @@ -69,9 +70,6 @@ type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; type UserProvided = Record>; type UiSettingsRaw = Record; -// identifier for current user -export const CURRENT_USER_PLACEHOLDER = ''; - /** * default scope read options, order matters */ @@ -90,13 +88,6 @@ const UiSettingScopeReadOptions = [ }, ] as ReadOptions[]; -export const generateDocId = (id: string, scope?: UiSettingScope) => { - if (scope === UiSettingScope.USER) { - return `${CURRENT_USER_PLACEHOLDER}_${id}`; - } - return id; -}; - export class UiSettingsClient implements IUiSettingsClient { private readonly type: UiSettingsServiceOptions['type']; private readonly id: UiSettingsServiceOptions['id']; @@ -174,6 +165,8 @@ export class UiSettingsClient implements IUiSettingsClient { } async setMany(changes: Record, scope?: UiSettingScope) { + // log changes and scope + this.log.debug(`UiSettingsClient.setMany: ${JSON.stringify({ changes, scope })}`); this.onWriteHook(changes, scope); if (scope) { diff --git a/src/core/server/ui_settings/utils.ts b/src/core/server/ui_settings/utils.ts new file mode 100644 index 000000000000..214f885c4a5c --- /dev/null +++ b/src/core/server/ui_settings/utils.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { UiSettingScope } from './types'; + +export const CURRENT_USER_PLACEHOLDER = ''; + +export const generateDocId = (id: string, scope?: UiSettingScope) => { + if (scope === UiSettingScope.USER) { + return `${CURRENT_USER_PLACEHOLDER}_${id}`; + } + return id; +}; diff --git a/src/core/server/utils/auth_info.ts b/src/core/server/utils/auth_info.ts new file mode 100644 index 000000000000..7d965ad05ca9 --- /dev/null +++ b/src/core/server/utils/auth_info.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthStatus } from '../http/auth_state_storage'; +import { OpenSearchDashboardsRequest } from '../http/router'; +import { HttpAuth } from '../http/types'; +import { PrincipalType, Principals } from '../saved_objects/permission_control/acl'; + +export interface AuthInfo { + backend_roles?: string[]; + user_name?: string; +} + +export const getPrincipalsFromRequest = ( + request: OpenSearchDashboardsRequest, + auth?: HttpAuth +): Principals => { + const payload: Principals = {}; + const authInfoResp = auth?.get(request); + if (authInfoResp?.status === AuthStatus.unknown) { + /** + * Login user have access to all the workspaces when no authentication is presented. + */ + return payload; + } + + if (authInfoResp?.status === AuthStatus.authenticated) { + const authState = authInfoResp?.state as { authInfo: AuthInfo } | null; + if (authState?.authInfo?.backend_roles) { + payload[PrincipalType.Groups] = authState.authInfo.backend_roles; + } + if (authState?.authInfo?.user_name) { + payload[PrincipalType.Users] = [authState.authInfo.user_name]; + } + return payload; + } + + if (authInfoResp?.status === AuthStatus.unauthenticated) { + throw new Error('NOT_AUTHORIZED'); + } + + throw new Error('UNEXPECTED_AUTHORIZATION_STATUS'); +}; diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts index a20b8c4c4e5b..64f8379a46e2 100644 --- a/src/core/server/utils/index.ts +++ b/src/core/server/utils/index.ts @@ -32,5 +32,6 @@ export * from './crypto'; export * from './from_root'; export * from './package_json'; export * from './streams'; +export { getPrincipalsFromRequest } from './auth_info'; export { getWorkspaceIdFromUrl, cleanWorkspaceId } from '../../utils'; export { updateWorkspaceState, getWorkspaceState } from './workspace'; diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index a7bcaabc323b..40924b850f59 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -49,6 +49,7 @@ import { getAriaName, toEditableConfig, DEFAULT_CATEGORY } from './lib'; import { FieldSetting, SettingsChanges } from './types'; import { NavigationPublicPluginStart } from '../../../../plugins/navigation/public'; +import { UiSettingScope } from '../../../../core/public'; interface AdvancedSettingsProps { enableSaving: boolean; @@ -172,7 +173,7 @@ export class AdvancedSettingsComponent extends Component< const all = config.getAll(); const userSettingsEnabled = config.get('theme:enableUserControl'); return Object.entries(all) - .filter((setting) => setting[1].scope !== 'user') + .filter((setting) => setting[1].scope !== UiSettingScope.USER) .map((setting) => { return toEditableConfig({ def: setting[1], diff --git a/src/plugins/advanced_settings/server/utils.ts b/src/plugins/advanced_settings/server/utils.ts index f50e3c981a78..7ebbb3598159 100644 --- a/src/plugins/advanced_settings/server/utils.ts +++ b/src/plugins/advanced_settings/server/utils.ts @@ -3,19 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthStatus, CoreStart, OpenSearchDashboardsRequest } from '../../../core/server'; - -export interface AuthInfo { - backend_roles?: string[]; - user_name?: string; -} +import { CoreStart, OpenSearchDashboardsRequest } from '../../../core/server'; +import { getPrincipalsFromRequest } from '../../../core/server/utils'; export const extractUserName = (request: OpenSearchDashboardsRequest, core?: CoreStart) => { - const authInfoResp = core?.http.auth.get(request); - - if (authInfoResp?.status === AuthStatus.authenticated) { - const authInfo = authInfoResp?.state as { authInfo: AuthInfo } | null; - return authInfo?.authInfo?.user_name; + try { + const principals = getPrincipalsFromRequest(request, core?.http.auth); + if (principals && principals.users?.length) { + return principals.users[0]; + } + } catch (error) { + return undefined; } - return undefined; }; From 06d29b859dcc54bb145cd6655cc6c2d5d61367e2 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Thu, 5 Sep 2024 23:52:24 +0800 Subject: [PATCH 15/21] add scope to ui settings route as query parameter Signed-off-by: Hailong Cui --- src/core/server/ui_settings/routes/delete.ts | 4 ++-- src/core/server/ui_settings/routes/get.ts | 4 ++-- src/core/server/ui_settings/routes/set.ts | 5 ++++- src/core/server/ui_settings/routes/set_many.ts | 5 ++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/server/ui_settings/routes/delete.ts b/src/core/server/ui_settings/routes/delete.ts index 22d01159c4ac..eb3d167edfd5 100644 --- a/src/core/server/ui_settings/routes/delete.ts +++ b/src/core/server/ui_settings/routes/delete.ts @@ -39,7 +39,7 @@ const validate = { params: schema.object({ key: schema.string(), }), - body: schema.object({ + query: schema.object({ scope: schema.maybe( schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) ), @@ -53,7 +53,7 @@ export function registerDeleteRoute(router: IRouter) { try { const uiSettingsClient = context.core.uiSettings.client; - const { scope } = request.body; + const { scope } = request.query; await uiSettingsClient.remove(request.params.key, scope); diff --git a/src/core/server/ui_settings/routes/get.ts b/src/core/server/ui_settings/routes/get.ts index ea7d1d555ca3..5d4571f93a1a 100644 --- a/src/core/server/ui_settings/routes/get.ts +++ b/src/core/server/ui_settings/routes/get.ts @@ -35,7 +35,7 @@ import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { UiSettingScope } from '../types'; const validate = { - body: schema.object({ + query: schema.object({ scope: schema.maybe( schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) ), @@ -48,7 +48,7 @@ export function registerGetRoute(router: IRouter) { async (context, request, response) => { try { const uiSettingsClient = context.core.uiSettings.client; - const { scope } = request.body; + const { scope } = request.query; return response.ok({ body: { settings: await uiSettingsClient.getUserProvided(scope), diff --git a/src/core/server/ui_settings/routes/set.ts b/src/core/server/ui_settings/routes/set.ts index 3beba658a332..abf49f4218e3 100644 --- a/src/core/server/ui_settings/routes/set.ts +++ b/src/core/server/ui_settings/routes/set.ts @@ -41,6 +41,8 @@ const validate = { }), body: schema.object({ value: schema.any(), + }), + query: schema.object({ scope: schema.maybe( schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) ), @@ -55,7 +57,8 @@ export function registerSetRoute(router: IRouter) { const uiSettingsClient = context.core.uiSettings.client; const { key } = request.params; - const { value, scope } = request.body; + const { value } = request.body; + const { scope } = request.query; await uiSettingsClient.set(key, value, scope); diff --git a/src/core/server/ui_settings/routes/set_many.ts b/src/core/server/ui_settings/routes/set_many.ts index 5100f2c6577d..b83d2bbef20e 100644 --- a/src/core/server/ui_settings/routes/set_many.ts +++ b/src/core/server/ui_settings/routes/set_many.ts @@ -38,6 +38,8 @@ import { UiSettingScope } from '../types'; const validate = { body: schema.object({ changes: schema.object({}, { unknowns: 'allow' }), + }), + query: schema.object({ scope: schema.maybe( schema.oneOf([schema.literal(UiSettingScope.GLOBAL), schema.literal(UiSettingScope.USER)]) ), @@ -51,7 +53,8 @@ export function registerSetManyRoute(router: IRouter) { try { const uiSettingsClient = context.core.uiSettings.client; - const { changes, scope } = request.body; + const { changes } = request.body; + const { scope } = request.query; await uiSettingsClient.setMany(changes, scope); From 0043a9b5b1defab47ed8b7860f05384bc0251367 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 6 Sep 2024 11:43:44 +0800 Subject: [PATCH 16/21] fix merge issue Signed-off-by: Hailong Cui --- src/core/server/ui_settings/ui_settings_client.ts | 4 +++- .../public/components/workspace_form/utils.test.ts | 8 ++------ src/plugins/workspace/server/ui_settings.ts | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 0c4f8f0e74f4..56fd42946749 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -72,6 +72,8 @@ type UiSettingsRaw = Record; /** * default scope read options, order matters + * for user setting, we don't want to create the record when user visit the dashboard that's get, + * that will have too much records without any actual setting in it, instead do create when write at first time */ const UiSettingScopeReadOptions = [ { @@ -172,7 +174,7 @@ export class UiSettingsClient implements IUiSettingsClient { if (scope) { await this.write({ changes, scope }); } else { - // group changes into by different scope + // group changes into different scope const [global, personal] = this.groupChanges(changes); if (global && Object.keys(global).length > 0) { await this.write({ changes: global }); diff --git a/src/plugins/workspace/public/components/workspace_form/utils.test.ts b/src/plugins/workspace/public/components/workspace_form/utils.test.ts index 03cea502f573..2d7430fae45d 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -12,12 +12,8 @@ import { isWorkspacePermissionSetting, } from './utils'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { - WorkspacePermissionItemType, - optionIdToWorkspacePermissionModesMap, - PermissionModeId, -} from './constants'; -import { DataSourceConnectionType } from '../../../common/types'; +import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; +import { DataSourceConnectionType, PermissionModeId } from '../../../common/types'; import { WorkspaceFormErrorCode } from './types'; describe('convertPermissionSettingsToPermissions', () => { diff --git a/src/plugins/workspace/server/ui_settings.ts b/src/plugins/workspace/server/ui_settings.ts index f34e867cb5f6..a5aceccc0386 100644 --- a/src/plugins/workspace/server/ui_settings.ts +++ b/src/plugins/workspace/server/ui_settings.ts @@ -6,12 +6,13 @@ import { schema } from '@osd/config-schema'; import { UiSettingsParams } from 'opensearch-dashboards/server'; +import { UiSettingScope } from '../../../core/server'; import { DEFAULT_WORKSPACE } from '../common/constants'; export const uiSettings: Record = { [DEFAULT_WORKSPACE]: { name: 'Default workspace', - scope: 'user', + scope: UiSettingScope.USER, value: null, type: 'string', schema: schema.nullable(schema.string()), From 3993fb3c3f04c9bd53957274fb454187cdb6748c Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 6 Sep 2024 11:44:09 +0800 Subject: [PATCH 17/21] remove version support for user settings Signed-off-by: Hailong Cui --- .../create_or_upgrade_saved_config.ts | 14 ++-- .../get_upgradeable_config.ts | 7 -- .../user_ui_settings_client_wrapper.test.ts | 78 ------------------- .../user_ui_settings_client_wrapper.ts | 25 +----- 4 files changed, 10 insertions(+), 114 deletions(-) diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index f4ba7d5176ef..d73bff2fab43 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -53,11 +53,15 @@ export async function createOrUpgradeSavedConfig( const { savedObjectsClient, version, buildNum, log, handleWriteErrors, scope } = options; // try to find an older config we can upgrade - const upgradeableConfig = await getUpgradeableConfig({ - savedObjectsClient, - version, - scope, - }); + let upgradeableConfig; + if (scope === UiSettingScope.USER) { + upgradeableConfig = undefined; + } else { + upgradeableConfig = await getUpgradeableConfig({ + savedObjectsClient, + version, + }); + } // default to the attributes of the upgradeableConfig if available const attributes = defaults( diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts index 3d248e275f6d..9e1100a91896 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts @@ -29,8 +29,6 @@ */ import { SavedObjectsClientContract } from '../../saved_objects/types'; -import { UiSettingScope } from '../types'; -import { CURRENT_USER_PLACEHOLDER } from '../utils'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; /** @@ -43,11 +41,9 @@ import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; export async function getUpgradeableConfig({ savedObjectsClient, version, - scope, }: { savedObjectsClient: SavedObjectsClientContract; version: string; - scope?: UiSettingScope; }) { // attempt to find a config we can upgrade const { saved_objects: savedConfigs } = await savedObjectsClient.find({ @@ -56,9 +52,6 @@ export async function getUpgradeableConfig({ perPage: 1000, sortField: 'buildNum', sortOrder: 'desc', - ...(scope && scope === UiSettingScope.USER - ? { hasReference: { type: 'user', id: CURRENT_USER_PLACEHOLDER } } - : {}), }); // try to find a config that we can upgrade diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts index 5f32fb200a2f..085919f5bd91 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts @@ -160,84 +160,6 @@ describe('UserUISettingsClientWrapper', () => { ); }); }); - - describe('#find', () => { - mockedClient.find.mockResolvedValue({ - saved_objects: [ - { - score: 1, - id: 'test_user_4.0.0', - type: 'config', - attributes: {}, - references: [], - }, - { - score: 1, - id: 'test_user_4.1.0', - type: 'config', - attributes: {}, - references: [], - }, - ], - total: 2, - per_page: 10, - page: 1, - }); - // beforeEach - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should replace user id', async () => { - const resp = await wrapperClient.find({ - type: 'config', - hasReference: { - type: 'user', - id: CURRENT_USER_PLACEHOLDER, - }, - }); - - expect(resp.saved_objects).toHaveLength(2); - expect(resp.saved_objects[0].id).toEqual('4.0.0'); - expect(resp.saved_objects[1].id).toEqual('4.1.0'); - - expect(mockedClient.find).toBeCalledWith({ - type: 'config', - hasReference: { - type: 'user', - id: 'test_user', - }, - }); - }); - - it('should not replace user id if not hasReference', async () => { - await wrapperClient.find({ - type: 'config', - }); - - expect(mockedClient.find).toBeCalledWith({ - type: 'config', - }); - }); - - it('should not replace user id if hasReference do not contains user id placeholder', async () => { - await wrapperClient.find({ - type: 'config', - hasReference: { - type: 'user', - id: 'test', - }, - }); - - expect(mockedClient.find).toBeCalledWith({ - type: 'config', - hasReference: { - type: 'user', - id: 'test', - }, - }); - }); - }); }); describe('UserUISettingsClientWrapper - security not enabled', () => { diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index 90bdbe321729..e68088b501b2 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -130,36 +130,13 @@ export class UserUISettingsClientWrapper { return wrapperOptions.client.create(type, attributes, options); }; - const findUiSettings = async ( - options: SavedObjectsFindOptions - ): Promise> => { - // check if options type is config - const userName = extractUserName(wrapperOptions.request, this.core); - const { hasReference } = options || {}; - if (options.type === 'config' && userName && hasReference) { - const id = hasReference.id.replace(CURRENT_USER_PLACEHOLDER, userName); - const resp: SavedObjectsFindResponse = await wrapperOptions.client.find({ - ...options, - hasReference: { ...hasReference, id }, - }); - - // normalize the document id to real version - resp.saved_objects.forEach((so) => { - so.id = so.id.replace(`${userName}_`, ''); - }); - - return Promise.resolve(resp); - } - return wrapperOptions.client.find(options); - }; - return { ...wrapperOptions.client, checkConflicts: wrapperOptions.client.checkConflicts, errors: wrapperOptions.client.errors, addToNamespaces: wrapperOptions.client.addToNamespaces, deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces, - find: findUiSettings, + find: wrapperOptions.client.find, bulkGet: wrapperOptions.client.bulkGet, create: createUiSettings, bulkCreate: wrapperOptions.client.bulkCreate, From a50975b1e23ea6b7c89b21bbbcdcdd9008e3a3b2 Mon Sep 17 00:00:00 2001 From: Cui Hailong Date: Fri, 6 Sep 2024 08:48:02 +0000 Subject: [PATCH 18/21] remove refrences from user settings Signed-off-by: Cui Hailong --- .../user_ui_settings_client_wrapper.test.ts | 14 -------------- .../user_ui_settings_client_wrapper.ts | 8 -------- 2 files changed, 22 deletions(-) diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts index 085919f5bd91..84fc47fd94b3 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.test.ts @@ -120,13 +120,6 @@ describe('UserUISettingsClientWrapper', () => { {}, { id: 'test_user', - references: [ - { - id: 'test_user', - name: 'test_user', - type: 'user', - }, - ], } ); }); @@ -144,13 +137,6 @@ describe('UserUISettingsClientWrapper', () => { {}, { id: 'test_user', - references: [ - { - id: 'test_user', - name: 'test_user', - type: 'user', - }, - ], permissions: { write: { users: ['test_user'], diff --git a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts index e68088b501b2..a17a32961034 100644 --- a/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts +++ b/src/plugins/advanced_settings/server/saved_objects/user_ui_settings_client_wrapper.ts @@ -107,17 +107,9 @@ export class UserUISettingsClientWrapper { // remove buildNum from attributes const { buildNum, ...others } = attributes as any; - // create with reference, the reference field will used for filter settings by user return await wrapperOptions.client.create(type, others, { ...options, id: docId, - references: [ - { - type: 'user', // dummy type - id: userName, - name: userName, - }, - ], ...(this.savedObjectsPermissionEnabled ? permissions : {}), }); } else { From e2dada680ce27e5a55f5c7be8e3265222145fbc4 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 6 Sep 2024 17:34:21 +0800 Subject: [PATCH 19/21] Update src/plugins/advanced_settings/public/management_app/advanced_settings.tsx Co-authored-by: Yulong Ruan Signed-off-by: Hailong Cui --- .../public/management_app/advanced_settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 40924b850f59..e517f779f5fc 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -173,7 +173,7 @@ export class AdvancedSettingsComponent extends Component< const all = config.getAll(); const userSettingsEnabled = config.get('theme:enableUserControl'); return Object.entries(all) - .filter((setting) => setting[1].scope !== UiSettingScope.USER) + .filter(([, setting]) => setting.scope !== UiSettingScope.USER) .map((setting) => { return toEditableConfig({ def: setting[1], From 099e1997f1fda16acf3c3a668f6bc5d00a57272e Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 6 Sep 2024 17:36:08 +0800 Subject: [PATCH 20/21] unify PermissionModeId Signed-off-by: Hailong Cui --- src/core/public/index.ts | 1 + src/core/server/index.ts | 2 +- src/plugins/workspace/common/types.ts | 6 ------ .../workspace/public/components/workspace_form/constants.ts | 2 +- .../workspace_form/workspace_permission_setting_input.tsx | 2 +- .../workspace_form/workspace_permission_setting_panel.tsx | 2 +- src/plugins/workspace/server/types.ts | 3 +-- src/plugins/workspace/server/utils.ts | 2 +- 8 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/core/public/index.ts b/src/core/public/index.ts index b813e12c97bf..0f177f672837 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -132,6 +132,7 @@ export { NavGroupStatus, WorkspaceAttributeWithPermission, UiSettingScope, + PermissionModeId, } from '../types'; export { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c697e15b9777..26dd30b8c6c4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -371,7 +371,7 @@ export { MetricsServiceStart, } from './metrics'; -export { AppCategory, WorkspaceAttribute } from '../types'; +export { AppCategory, WorkspaceAttribute, PermissionModeId } from '../types'; export { DEFAULT_APP_CATEGORIES, WORKSPACE_TYPE, DEFAULT_NAV_GROUPS } from '../utils'; export { diff --git a/src/plugins/workspace/common/types.ts b/src/plugins/workspace/common/types.ts index 06f5c602bda6..4b64f62d9e29 100644 --- a/src/plugins/workspace/common/types.ts +++ b/src/plugins/workspace/common/types.ts @@ -27,9 +27,3 @@ export interface DataSourceConnection { description?: string; relatedConnections?: DataSourceConnection[]; } - -export enum PermissionModeId { - Read = 'read', - ReadAndWrite = 'read+write', - Owner = 'owner', -} diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index ca72f560bb75..eb1aa1737592 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -5,7 +5,7 @@ import { i18n } from '@osd/i18n'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { PermissionModeId } from '../../../common/types'; +import { PermissionModeId } from '../../../../../core/public'; export enum WorkspaceOperationType { Create = 'create', diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx index f819caac1f6a..bbbc5e2b393d 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx @@ -22,7 +22,7 @@ import { PERMISSION_ACCESS_LEVEL_LABEL_ID, } from './constants'; import { getPermissionModeId } from './utils'; -import { PermissionModeId } from '../../../common/types'; +import { PermissionModeId } from '../../../../../core/public'; const permissionModeOptions: Array> = [ { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx index c6047ae0d9ff..6b10b88dc5f5 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx @@ -26,7 +26,7 @@ import { WorkspacePermissionSettingInputProps, } from './workspace_permission_setting_input'; import { generateNextPermissionSettingsId } from './utils'; -import { PermissionModeId } from '../../../common/types'; +import { PermissionModeId } from '../../../../../core/public'; export interface WorkspacePermissionSettingPanelProps { errors?: { [key: number]: WorkspaceFormError }; diff --git a/src/plugins/workspace/server/types.ts b/src/plugins/workspace/server/types.ts index 0f884afecd78..ed056bedf4e3 100644 --- a/src/plugins/workspace/server/types.ts +++ b/src/plugins/workspace/server/types.ts @@ -6,7 +6,6 @@ import { Logger, OpenSearchDashboardsRequest, - RequestHandlerContext, SavedObjectsFindResponse, CoreSetup, WorkspaceAttribute, @@ -14,7 +13,7 @@ import { Permissions, UiSettingsServiceStart, } from '../../../core/server'; -import { PermissionModeId } from '../common/types'; +import { PermissionModeId } from '../../../core/server'; export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { permissions?: Permissions; permissionMode?: PermissionModeId; diff --git a/src/plugins/workspace/server/utils.ts b/src/plugins/workspace/server/utils.ts index e98ae806ed50..59bb079f0f46 100644 --- a/src/plugins/workspace/server/utils.ts +++ b/src/plugins/workspace/server/utils.ts @@ -16,7 +16,7 @@ import { import { updateWorkspaceState } from '../../../core/server/utils'; import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../data_source_management/common'; import { CURRENT_USER_PLACEHOLDER, WorkspacePermissionMode } from '../common/constants'; -import { PermissionModeId } from '../common/types'; +import { PermissionModeId } from '../../../core/server'; /** * Generate URL friendly random ID From 5e83f3f700ea3001bc9981e8132586d826b484d0 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 6 Sep 2024 18:28:41 +0800 Subject: [PATCH 21/21] fix ut Signed-off-by: Hailong Cui --- .../workspace/public/components/workspace_form/utils.test.ts | 3 ++- .../workspace/public/components/workspace_form/utils.ts | 3 ++- src/plugins/workspace/server/utils.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/utils.test.ts b/src/plugins/workspace/public/components/workspace_form/utils.test.ts index 2d7430fae45d..7671f08ee706 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -13,8 +13,9 @@ import { } from './utils'; import { WorkspacePermissionMode } from '../../../common/constants'; import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; -import { DataSourceConnectionType, PermissionModeId } from '../../../common/types'; +import { DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormErrorCode } from './types'; +import { PermissionModeId } from '../../../../../core/public'; describe('convertPermissionSettingsToPermissions', () => { it('should return undefined if permission items not provided', () => { diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index da33e18d29b3..79c5501e9102 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -24,8 +24,9 @@ import { WorkspaceUserGroupPermissionSetting, WorkspaceUserPermissionSetting, } from './types'; -import { DataSourceConnection, PermissionModeId } from '../../../common/types'; +import { DataSourceConnection } from '../../../common/types'; import { validateWorkspaceColor } from '../../../common/utils'; +import { PermissionModeId } from '../../../../../core/public'; export const isValidFormTextInput = (input?: string) => { /** diff --git a/src/plugins/workspace/server/utils.ts b/src/plugins/workspace/server/utils.ts index 59bb079f0f46..118cb9d8909d 100644 --- a/src/plugins/workspace/server/utils.ts +++ b/src/plugins/workspace/server/utils.ts @@ -12,6 +12,7 @@ import { Permissions, SavedObjectsClientContract, IUiSettingsClient, + Principals, } from '../../../core/server'; import { updateWorkspaceState } from '../../../core/server/utils'; import { DEFAULT_DATA_SOURCE_UI_SETTINGS_ID } from '../../data_source_management/common';