From 8e5930a03c9698bd3b2f5b40b520060fa287d3cc Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Mon, 15 Apr 2024 23:35:47 +0800 Subject: [PATCH 1/3] fix(workspace): apps are missing when updating a workspace This is caused by #6234 which marked apps as inaccessible when the apps are not configured into the current workspace. However, inaccessible apps can not be configured into a workspace. Signed-off-by: Yulong Ruan --- src/plugins/workspace/public/application.tsx | 9 ++++- .../public/components/workspace_form/types.ts | 1 + .../components/workspace_form/utils.test.ts | 37 +++++++++++++++++++ .../public/components/workspace_form/utils.ts | 18 +++++++-- .../workspace_feature_selector.tsx | 9 +++-- .../workspace_form/workspace_form.tsx | 1 + .../components/workspace_updater/index.tsx | 2 +- .../workspace_updater/workspace_updater.tsx | 10 ++++- .../components/workspace_updater_app.tsx | 6 +-- src/plugins/workspace/public/plugin.ts | 24 +++++++++++- 10 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index 5440d98e6945..f85a0ec672c9 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -11,6 +11,7 @@ import { WorkspaceFatalError } from './components/workspace_fatal_error'; import { WorkspaceCreatorApp } from './components/workspace_creator_app'; import { WorkspaceUpdaterApp } from './components/workspace_updater_app'; import { WorkspaceListApp } from './components/workspace_list_app'; +import { WorkspaceUpdaterProps } from './components/workspace_updater'; import { Services } from './types'; export const renderCreatorApp = ({ element }: AppMountParameters, services: Services) => { @@ -26,10 +27,14 @@ export const renderCreatorApp = ({ element }: AppMountParameters, services: Serv }; }; -export const renderUpdaterApp = ({ element }: AppMountParameters, services: Services) => { +export const renderUpdaterApp = ( + { element }: AppMountParameters, + services: Services, + props: WorkspaceUpdaterProps +) => { ReactDOM.render( - + , element ); diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts index 8014c2321ad5..592102eecada 100644 --- a/src/plugins/workspace/public/components/workspace_form/types.ts +++ b/src/plugins/workspace/public/components/workspace_form/types.ts @@ -35,4 +35,5 @@ export interface WorkspaceFormProps { onSubmit?: (formData: WorkspaceFormSubmitData) => void; defaultValues?: WorkspaceFormData; operationType?: WorkspaceOperationType; + restrictedApps?: Set; } 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 6101bd078831..babaea2208e6 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -7,6 +7,43 @@ import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../../../core/pu import { convertApplicationsToFeaturesOrGroups } from './utils'; describe('convertApplicationsToFeaturesOrGroups', () => { + it('should not filter out restrict Apps', () => { + expect( + convertApplicationsToFeaturesOrGroups( + [ + { id: 'foo1', title: 'Foo 1', navLinkStatus: AppNavLinkStatus.hidden }, + { id: 'foo2', title: 'Foo 2', navLinkStatus: AppNavLinkStatus.visible, chromeless: true }, + { + id: 'foo3', + title: 'Foo 3', + navLinkStatus: AppNavLinkStatus.visible, + category: DEFAULT_APP_CATEGORIES.management, + }, + { + id: 'workspace_overview', + title: 'Workspace Overview', + navLinkStatus: AppNavLinkStatus.visible, + }, + { + id: 'bar', + title: 'Bar', + navLinkStatus: AppNavLinkStatus.visible, + }, + ], + new Set(['foo1']) + ) + ).toEqual([ + { + id: 'foo1', + name: 'Foo 1', + }, + { + id: 'bar', + name: 'Bar', + }, + ]); + }); + it('should filter out invisible features', () => { expect( convertApplicationsToFeaturesOrGroups([ diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 5514e6a8fb9c..1f9d202769fd 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -44,18 +44,28 @@ export const getNumberOfErrors = (formErrors: WorkspaceFormErrors) => { export const convertApplicationsToFeaturesOrGroups = ( applications: Array< Pick - > + >, + restrictedApps?: Set ) => { const UNDEFINED = 'undefined'; // Filter out all hidden applications and management applications and default selected features - const visibleApplications = applications.filter( - ({ navLinkStatus, chromeless, category, id }) => + const visibleApplications = applications.filter(({ navLinkStatus, chromeless, category, id }) => { + /** + * Restrict apps are apps that can be configured into a workspace, but restrict to access + * because the current workspace didn't have the apps configured, such apps should NOT filter out + */ + if (restrictedApps && restrictedApps.has(id)) { + return true; + } + + return ( navLinkStatus !== AppNavLinkStatus.hidden && !chromeless && !DEFAULT_SELECTED_FEATURES_IDS.includes(id) && category?.id !== DEFAULT_APP_CATEGORIES.management.id - ); + ); + }); /** * diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx index da9deb174f52..e860d3a34b53 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx @@ -24,16 +24,19 @@ export interface WorkspaceFeatureSelectorProps { >; selectedFeatures: string[]; onChange: (newFeatures: string[]) => void; + restrictedApps?: Set; } export const WorkspaceFeatureSelector = ({ applications, selectedFeatures, onChange, + restrictedApps, }: WorkspaceFeatureSelectorProps) => { - const featuresOrGroups = useMemo(() => convertApplicationsToFeaturesOrGroups(applications), [ - applications, - ]); + const featuresOrGroups = useMemo( + () => convertApplicationsToFeaturesOrGroups(applications, restrictedApps), + [applications, restrictedApps] + ); const handleFeatureChange = useCallback( (featureId) => { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx index 69793c75395d..be50228512f2 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -136,6 +136,7 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { applications={applications} selectedFeatures={formData.features} onChange={handleFeaturesChange} + restrictedApps={props.restrictedApps} /> )} diff --git a/src/plugins/workspace/public/components/workspace_updater/index.tsx b/src/plugins/workspace/public/components/workspace_updater/index.tsx index 711f19fd25f6..b00065a00f64 100644 --- a/src/plugins/workspace/public/components/workspace_updater/index.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/index.tsx @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { WorkspaceUpdater } from './workspace_updater'; +export { WorkspaceUpdater, WorkspaceUpdaterProps } from './workspace_updater'; diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index d39ddd650360..b877ccc8191b 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -8,7 +8,7 @@ import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eu import { i18n } from '@osd/i18n'; import { WorkspaceAttribute } from 'opensearch-dashboards/public'; import { useObservable } from 'react-use'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form'; import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants'; @@ -16,18 +16,23 @@ import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { WorkspaceClient } from '../../workspace_client'; import { WorkspaceFormData } from '../workspace_form/types'; +export interface WorkspaceUpdaterProps { + restrictedApps$?: BehaviorSubject>; +} + function getFormDataFromWorkspace( currentWorkspace: WorkspaceAttribute | null | undefined ): WorkspaceFormData { return (currentWorkspace || {}) as WorkspaceFormData; } -export const WorkspaceUpdater = () => { +export const WorkspaceUpdater = (props: WorkspaceUpdaterProps) => { const { services: { application, workspaces, notifications, http, workspaceClient }, } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); const currentWorkspace = useObservable(workspaces ? workspaces.currentWorkspace$ : of(null)); + const restrictedApps = useObservable(props.restrictedApps$ ?? of(undefined)); const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState( getFormDataFromWorkspace(currentWorkspace) ); @@ -108,6 +113,7 @@ export const WorkspaceUpdater = () => { defaultValues={currentWorkspaceFormData} onSubmit={handleWorkspaceFormSubmit} operationType={WorkspaceOperationType.Update} + restrictedApps={restrictedApps} /> )} diff --git a/src/plugins/workspace/public/components/workspace_updater_app.tsx b/src/plugins/workspace/public/components/workspace_updater_app.tsx index e16c9ad72e0f..ab106b5c4b7a 100644 --- a/src/plugins/workspace/public/components/workspace_updater_app.tsx +++ b/src/plugins/workspace/public/components/workspace_updater_app.tsx @@ -7,9 +7,9 @@ import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { WorkspaceUpdater } from './workspace_updater'; +import { WorkspaceUpdater, WorkspaceUpdaterProps } from './workspace_updater'; -export const WorkspaceUpdaterApp = () => { +export const WorkspaceUpdaterApp = (props: WorkspaceUpdaterProps) => { const { services: { chrome }, } = useOpenSearchDashboards(); @@ -29,7 +29,7 @@ export const WorkspaceUpdaterApp = () => { return ( - + ); }; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index b27c4b3bdd4a..52ba42feb31e 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,6 +14,7 @@ import { AppNavLinkStatus, AppUpdater, AppStatus, + DEFAULT_APP_CATEGORIES, } from '../../../core/public'; import { WORKSPACE_FATAL_ERROR_APP_ID, @@ -30,7 +31,11 @@ import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; import { isAppAccessibleInWorkspace } from './utils'; -type WorkspaceAppType = (params: AppMountParameters, services: Services) => () => void; +type WorkspaceAppType = ( + params: AppMountParameters, + services: Services, + props: Record +) => () => void; interface WorkspacePluginSetupDeps { savedObjectsManagement?: SavedObjectsManagementPluginSetup; @@ -41,6 +46,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> private currentWorkspaceSubscription?: Subscription; private currentWorkspaceIdSubscription?: Subscription; private appUpdater$ = new BehaviorSubject(() => undefined); + private restrictedApps$ = new BehaviorSubject(new Set()); private _changeSavedObjectCurrentWorkspace() { if (this.coreStart) { return this.coreStart.workspaces.currentWorkspaceId$.subscribe((currentWorkspaceId) => { @@ -65,6 +71,20 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> if (isAppAccessibleInWorkspace(app, currentWorkspace)) { return; } + + if (app.status === AppStatus.inaccessible) { + return; + } + + /** + * Restricted apps can be configured into a workspace, but they are not configured by the + * current workspace. Apps of management category can NOT configured into a workspace, so + * needs to be excluded. + */ + if (app.category?.id !== DEFAULT_APP_CATEGORIES.management.id) { + this.restrictedApps$.next(this.restrictedApps$.value.add(app.id)); + } + /** * Change the app to `inaccessible` if it is not configured in the workspace * If trying to access such app, an "Application Not Found" page will be displayed @@ -129,7 +149,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> workspaceClient, }; - return renderApp(params, services); + return renderApp(params, services, { restrictedApps$: this.restrictedApps$ }); }; // create From 79da6af1df93db5b55eb16b22cd4765d01b3f53c Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 16 Apr 2024 17:48:45 +0800 Subject: [PATCH 2/3] refactor(workspace): store workspace configurable apps in a global variable Signed-off-by: Yulong Ruan --- src/plugins/workspace/public/application.tsx | 9 ++- .../workspace_creator.test.tsx | 48 +++++++++++--- .../workspace_creator/workspace_creator.tsx | 14 +++- .../components/workspace_creator_app.tsx | 5 +- .../public/components/workspace_form/types.ts | 4 +- .../components/workspace_form/utils.test.ts | 66 ------------------- .../public/components/workspace_form/utils.ts | 29 +------- .../workspace_feature_selector.test.tsx | 12 +++- .../workspace_feature_selector.tsx | 12 ++-- .../workspace_form/workspace_form.tsx | 4 +- .../workspace_updater.test.tsx | 50 +++++++++++--- .../workspace_updater/workspace_updater.tsx | 10 +-- src/plugins/workspace/public/plugin.ts | 43 +++++++----- src/plugins/workspace/public/utils.ts | 23 ++++++- 14 files changed, 179 insertions(+), 150 deletions(-) diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index f85a0ec672c9..83c287357bbe 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -13,11 +13,16 @@ import { WorkspaceUpdaterApp } from './components/workspace_updater_app'; import { WorkspaceListApp } from './components/workspace_list_app'; import { WorkspaceUpdaterProps } from './components/workspace_updater'; import { Services } from './types'; +import { WorkspaceCreatorProps } from './components/workspace_creator/workspace_creator'; -export const renderCreatorApp = ({ element }: AppMountParameters, services: Services) => { +export const renderCreatorApp = ( + { element }: AppMountParameters, + services: Services, + props: WorkspaceCreatorProps +) => { ReactDOM.render( - + , element ); diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx index 9cc4f9b53f69..d6c972f8d9de 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx @@ -88,13 +88,21 @@ describe('WorkspaceCreator', () => { }); it('should not create workspace when name is empty', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); expect(workspaceClientCreate).not.toHaveBeenCalled(); }); it('should not create workspace with invalid name', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: '~' }, @@ -103,7 +111,11 @@ describe('WorkspaceCreator', () => { }); it('should not create workspace with invalid description', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -116,7 +128,11 @@ describe('WorkspaceCreator', () => { }); it('cancel create workspace', async () => { - const { findByText, getByTestId } = render(); + const { findByText, getByTestId } = render( + + ); fireEvent.click(getByTestId('workspaceForm-bottomBar-cancelButton')); await findByText('Discard changes?'); fireEvent.click(getByTestId('confirmModalConfirmButton')); @@ -124,7 +140,11 @@ describe('WorkspaceCreator', () => { }); it('create workspace with detailed information', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -155,7 +175,11 @@ describe('WorkspaceCreator', () => { it('create workspace with customized features', async () => { setHrefSpy.mockReset(); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -181,7 +205,11 @@ describe('WorkspaceCreator', () => { it('should show danger toasts after create workspace failed', async () => { workspaceClientCreate.mockReturnValue({ result: { id: 'failResult' }, success: false }); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -198,7 +226,11 @@ describe('WorkspaceCreator', () => { workspaceClientCreate.mockImplementation(async () => { throw new Error(); }); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index 0ba7ca9947df..176971172590 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -6,16 +6,27 @@ import React, { useCallback } from 'react'; import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { useObservable } from 'react-use'; + +import { PublicAppInfo } from 'opensearch-dashboards/public'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form'; import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants'; import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { WorkspaceClient } from '../../workspace_client'; +import { BehaviorSubject, of } from 'rxjs'; + +export interface WorkspaceCreatorProps { + workspaceConfigurableApps$?: BehaviorSubject; +} -export const WorkspaceCreator = () => { +export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { const { services: { application, notifications, http, workspaceClient }, } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); + const workspaceConfigurableApps = useObservable( + props.workspaceConfigurableApps$ ?? of(undefined) + ); const handleWorkspaceFormSubmit = useCallback( async (data: WorkspaceFormSubmitData) => { @@ -80,6 +91,7 @@ export const WorkspaceCreator = () => { application={application} onSubmit={handleWorkspaceFormSubmit} operationType={WorkspaceOperationType.Create} + workspaceConfigurableApps={workspaceConfigurableApps} /> )} diff --git a/src/plugins/workspace/public/components/workspace_creator_app.tsx b/src/plugins/workspace/public/components/workspace_creator_app.tsx index b74359929352..e384f5d5bfed 100644 --- a/src/plugins/workspace/public/components/workspace_creator_app.tsx +++ b/src/plugins/workspace/public/components/workspace_creator_app.tsx @@ -8,8 +8,9 @@ import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; import { WorkspaceCreator } from './workspace_creator'; +import { WorkspaceCreatorProps } from './workspace_creator/workspace_creator'; -export const WorkspaceCreatorApp = () => { +export const WorkspaceCreatorApp = (props: WorkspaceCreatorProps) => { const { services: { chrome }, } = useOpenSearchDashboards(); @@ -29,7 +30,7 @@ export const WorkspaceCreatorApp = () => { return ( - + ); }; diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts index 592102eecada..d29048d9364f 100644 --- a/src/plugins/workspace/public/components/workspace_form/types.ts +++ b/src/plugins/workspace/public/components/workspace_form/types.ts @@ -4,7 +4,7 @@ */ import type { WorkspaceOperationType } from './constants'; -import type { ApplicationStart } from '../../../../../core/public'; +import type { ApplicationStart, PublicAppInfo } from '../../../../../core/public'; export interface WorkspaceFormSubmitData { name: string; @@ -35,5 +35,5 @@ export interface WorkspaceFormProps { onSubmit?: (formData: WorkspaceFormSubmitData) => void; defaultValues?: WorkspaceFormData; operationType?: WorkspaceOperationType; - restrictedApps?: Set; + workspaceConfigurableApps?: PublicAppInfo[]; } 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 babaea2208e6..adc5a11bbc40 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -7,72 +7,6 @@ import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../../../core/pu import { convertApplicationsToFeaturesOrGroups } from './utils'; describe('convertApplicationsToFeaturesOrGroups', () => { - it('should not filter out restrict Apps', () => { - expect( - convertApplicationsToFeaturesOrGroups( - [ - { id: 'foo1', title: 'Foo 1', navLinkStatus: AppNavLinkStatus.hidden }, - { id: 'foo2', title: 'Foo 2', navLinkStatus: AppNavLinkStatus.visible, chromeless: true }, - { - id: 'foo3', - title: 'Foo 3', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.management, - }, - { - id: 'workspace_overview', - title: 'Workspace Overview', - navLinkStatus: AppNavLinkStatus.visible, - }, - { - id: 'bar', - title: 'Bar', - navLinkStatus: AppNavLinkStatus.visible, - }, - ], - new Set(['foo1']) - ) - ).toEqual([ - { - id: 'foo1', - name: 'Foo 1', - }, - { - id: 'bar', - name: 'Bar', - }, - ]); - }); - - it('should filter out invisible features', () => { - expect( - convertApplicationsToFeaturesOrGroups([ - { id: 'foo1', title: 'Foo 1', navLinkStatus: AppNavLinkStatus.hidden }, - { id: 'foo2', title: 'Foo 2', navLinkStatus: AppNavLinkStatus.visible, chromeless: true }, - { - id: 'foo3', - title: 'Foo 3', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.management, - }, - { - id: 'workspace_overview', - title: 'Workspace Overview', - navLinkStatus: AppNavLinkStatus.visible, - }, - { - id: 'bar', - title: 'Bar', - navLinkStatus: AppNavLinkStatus.visible, - }, - ]) - ).toEqual([ - { - id: 'bar', - name: 'Bar', - }, - ]); - }); it('should group same category applications in same feature group', () => { expect( convertApplicationsToFeaturesOrGroups([ diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 1f9d202769fd..7750686a1994 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -3,11 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { - AppNavLinkStatus, - DEFAULT_APP_CATEGORIES, - PublicAppInfo, -} from '../../../../../core/public'; +import { PublicAppInfo } from '../../../../../core/public'; import { DEFAULT_SELECTED_FEATURES_IDS } from '../../../common/constants'; import { WorkspaceFeature, WorkspaceFeatureGroup, WorkspaceFormErrors } from './types'; @@ -44,29 +40,10 @@ export const getNumberOfErrors = (formErrors: WorkspaceFormErrors) => { export const convertApplicationsToFeaturesOrGroups = ( applications: Array< Pick - >, - restrictedApps?: Set + > ) => { const UNDEFINED = 'undefined'; - // Filter out all hidden applications and management applications and default selected features - const visibleApplications = applications.filter(({ navLinkStatus, chromeless, category, id }) => { - /** - * Restrict apps are apps that can be configured into a workspace, but restrict to access - * because the current workspace didn't have the apps configured, such apps should NOT filter out - */ - if (restrictedApps && restrictedApps.has(id)) { - return true; - } - - return ( - navLinkStatus !== AppNavLinkStatus.hidden && - !chromeless && - !DEFAULT_SELECTED_FEATURES_IDS.includes(id) && - category?.id !== DEFAULT_APP_CATEGORIES.management.id - ); - }); - /** * * Convert applications to features map, the map use category label as @@ -74,7 +51,7 @@ export const convertApplicationsToFeaturesOrGroups = ( * transfer application to feature. * **/ - const categoryLabel2Features = visibleApplications.reduce<{ + const categoryLabel2Features = applications.reduce<{ [key: string]: WorkspaceFeature[]; }>((previousValue, application) => { const label = application.category?.label || UNDEFINED; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx index 0875b0d1ff10..313d459b6018 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx @@ -9,7 +9,7 @@ import { WorkspaceFeatureSelector, WorkspaceFeatureSelectorProps, } from './workspace_feature_selector'; -import { AppNavLinkStatus } from '../../../../../core/public'; +import { AppNavLinkStatus, AppStatus } from '../../../../../core/public'; const setup = (options?: Partial) => { const onChangeMock = jest.fn(); @@ -19,28 +19,36 @@ const setup = (options?: Partial) => { title: 'App 1', category: { id: 'category-1', label: 'Category 1' }, navLinkStatus: AppNavLinkStatus.visible, + status: AppStatus.accessible, + appRoute: '/app-1', }, { id: 'app-2', title: 'App 2', category: { id: 'category-1', label: 'Category 1' }, navLinkStatus: AppNavLinkStatus.visible, + status: AppStatus.accessible, + appRoute: '/app-2', }, { id: 'app-3', title: 'App 3', category: { id: 'category-2', label: 'Category 2' }, navLinkStatus: AppNavLinkStatus.visible, + status: AppStatus.accessible, + appRoute: '/app-3', }, { id: 'app-4', title: 'App 4', navLinkStatus: AppNavLinkStatus.visible, + status: AppStatus.accessible, + appRoute: '/app-4', }, ]; const renderResult = render( - >; selectedFeatures: string[]; onChange: (newFeatures: string[]) => void; - restrictedApps?: Set; + workspaceConfigurableApps?: PublicAppInfo[]; } export const WorkspaceFeatureSelector = ({ - applications, selectedFeatures, onChange, - restrictedApps, + workspaceConfigurableApps, }: WorkspaceFeatureSelectorProps) => { const featuresOrGroups = useMemo( - () => convertApplicationsToFeaturesOrGroups(applications, restrictedApps), - [applications, restrictedApps] + () => convertApplicationsToFeaturesOrGroups(workspaceConfigurableApps ?? []), + [workspaceConfigurableApps] ); const handleFeatureChange = useCallback( diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx index be50228512f2..b6f93bf3acff 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -32,7 +32,6 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { formData, formErrors, selectedTab, - applications, numberOfErrors, handleFormSubmit, handleColorChange, @@ -133,10 +132,9 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { )} diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.test.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.test.tsx index d829154426dd..0f86a800a309 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.test.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.test.tsx @@ -113,12 +113,21 @@ describe('WorkspaceUpdater', () => { it('cannot render when the name of the current workspace is empty', async () => { const mockedWorkspacesService = workspacesServiceMock.createSetupContract(); - const { container } = render(); + const { container } = render( + + ); expect(container).toMatchInlineSnapshot(`
`); }); it('cannot update workspace with invalid name', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: '~' }, @@ -127,7 +136,11 @@ describe('WorkspaceUpdater', () => { }); it('cannot update workspace with invalid description', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -140,7 +153,11 @@ describe('WorkspaceUpdater', () => { }); it('cancel update workspace', async () => { - const { findByText, getByTestId } = render(); + const { findByText, getByTestId } = render( + + ); fireEvent.click(getByTestId('workspaceForm-bottomBar-cancelButton')); await findByText('Discard changes?'); fireEvent.click(getByTestId('confirmModalConfirmButton')); @@ -148,7 +165,11 @@ describe('WorkspaceUpdater', () => { }); it('update workspace successfully', async () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -190,7 +211,11 @@ describe('WorkspaceUpdater', () => { it('should show danger toasts after update workspace failed', async () => { workspaceClientUpdate.mockReturnValue({ result: false, success: false }); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -207,7 +232,11 @@ describe('WorkspaceUpdater', () => { workspaceClientUpdate.mockImplementation(() => { throw new Error('update workspace failed'); }); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, @@ -222,7 +251,12 @@ describe('WorkspaceUpdater', () => { it('should show danger toasts when currentWorkspace is missing after click update button', async () => { const mockedWorkspacesService = workspacesServiceMock.createSetupContract(); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { diff --git a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx index b877ccc8191b..c79a66e7b40a 100644 --- a/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx +++ b/src/plugins/workspace/public/components/workspace_updater/workspace_updater.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import { WorkspaceAttribute } from 'opensearch-dashboards/public'; +import { PublicAppInfo, WorkspaceAttribute } from 'opensearch-dashboards/public'; import { useObservable } from 'react-use'; import { BehaviorSubject, of } from 'rxjs'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; @@ -17,7 +17,7 @@ import { WorkspaceClient } from '../../workspace_client'; import { WorkspaceFormData } from '../workspace_form/types'; export interface WorkspaceUpdaterProps { - restrictedApps$?: BehaviorSubject>; + workspaceConfigurableApps$?: BehaviorSubject; } function getFormDataFromWorkspace( @@ -32,7 +32,9 @@ export const WorkspaceUpdater = (props: WorkspaceUpdaterProps) => { } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); const currentWorkspace = useObservable(workspaces ? workspaces.currentWorkspace$ : of(null)); - const restrictedApps = useObservable(props.restrictedApps$ ?? of(undefined)); + const workspaceConfigurableApps = useObservable( + props.workspaceConfigurableApps$ ?? of(undefined) + ); const [currentWorkspaceFormData, setCurrentWorkspaceFormData] = useState( getFormDataFromWorkspace(currentWorkspace) ); @@ -113,7 +115,7 @@ export const WorkspaceUpdater = (props: WorkspaceUpdaterProps) => { defaultValues={currentWorkspaceFormData} onSubmit={handleWorkspaceFormSubmit} operationType={WorkspaceOperationType.Update} - restrictedApps={restrictedApps} + workspaceConfigurableApps={workspaceConfigurableApps} /> )} diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 52ba42feb31e..a615b71cf4e5 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -14,7 +14,7 @@ import { AppNavLinkStatus, AppUpdater, AppStatus, - DEFAULT_APP_CATEGORIES, + PublicAppInfo, } from '../../../core/public'; import { WORKSPACE_FATAL_ERROR_APP_ID, @@ -29,7 +29,8 @@ import { WorkspaceClient } from './workspace_client'; import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_objects_management/public'; import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; -import { isAppAccessibleInWorkspace } from './utils'; +import { filterWorkspaceConfigurableApps, isAppAccessibleInWorkspace } from './utils'; +import { first } from 'rxjs/operators'; type WorkspaceAppType = ( params: AppMountParameters, @@ -46,7 +47,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> private currentWorkspaceSubscription?: Subscription; private currentWorkspaceIdSubscription?: Subscription; private appUpdater$ = new BehaviorSubject(() => undefined); - private restrictedApps$ = new BehaviorSubject(new Set()); + private workspaceConfigurableApps$ = new BehaviorSubject([]); private _changeSavedObjectCurrentWorkspace() { if (this.coreStart) { return this.coreStart.workspaces.currentWorkspaceId$.subscribe((currentWorkspaceId) => { @@ -61,7 +62,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> * Filter nav links by the current workspace, once the current workspace change, the nav links(left nav bar) * should also be updated according to the configured features of the current workspace */ - private filterNavLinks(core: CoreStart) { + private filterNavLinks = (core: CoreStart) => { const currentWorkspace$ = core.workspaces.currentWorkspace$; this.currentWorkspaceSubscription?.unsubscribe(); @@ -76,15 +77,6 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> return; } - /** - * Restricted apps can be configured into a workspace, but they are not configured by the - * current workspace. Apps of management category can NOT configured into a workspace, so - * needs to be excluded. - */ - if (app.category?.id !== DEFAULT_APP_CATEGORIES.management.id) { - this.restrictedApps$.next(this.restrictedApps$.value.add(app.id)); - } - /** * Change the app to `inaccessible` if it is not configured in the workspace * If trying to access such app, an "Application Not Found" page will be displayed @@ -93,7 +85,20 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> }); } }); - } + }; + + /** + * Initiate an observable with the value of all applications which can be configured by workspace + */ + private setWorkspaceConfigurableApps = async (core: CoreStart) => { + const allApps = await new Promise((resolve) => { + core.application.applications$.pipe(first()).subscribe((apps) => { + resolve([...apps.values()]); + }); + }); + const availableApps = filterWorkspaceConfigurableApps(allApps); + this.workspaceConfigurableApps$.next(availableApps); + }; public async setup(core: CoreSetup, { savedObjectsManagement }: WorkspacePluginSetupDeps) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); @@ -149,7 +154,9 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> workspaceClient, }; - return renderApp(params, services, { restrictedApps$: this.restrictedApps$ }); + return renderApp(params, services, { + workspaceConfigurableApps$: this.workspaceConfigurableApps$, + }); }; // create @@ -223,8 +230,10 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> this.currentWorkspaceIdSubscription = this._changeSavedObjectCurrentWorkspace(); - // When starts, filter the nav links based on the current workspace - this.filterNavLinks(core); + this.setWorkspaceConfigurableApps(core).then(() => { + // filter the nav links based on the current workspace + this.filterNavLinks(core); + }); return {}; } diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index e70a26028525..dd0cb8275469 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -3,7 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { App, AppCategory, AppNavLinkStatus, WorkspaceObject } from '../../../core/public'; +import { + App, + AppCategory, + AppNavLinkStatus, + DEFAULT_APP_CATEGORIES, + PublicAppInfo, + WorkspaceObject, +} from '../../../core/public'; +import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; /** * Checks if a given feature matches the provided feature configuration. @@ -99,3 +107,16 @@ export function isAppAccessibleInWorkspace(app: App, workspace: WorkspaceObject) } return false; } + +export const filterWorkspaceConfigurableApps = (applications: PublicAppInfo[]) => { + const visibleApplications = applications.filter(({ navLinkStatus, chromeless, category, id }) => { + return ( + navLinkStatus !== AppNavLinkStatus.hidden && + !chromeless && + !DEFAULT_SELECTED_FEATURES_IDS.includes(id) && + category?.id !== DEFAULT_APP_CATEGORIES.management.id + ); + }); + + return visibleApplications; +}; From f3cbe9a954d8efeead75f422d631b288192a6268 Mon Sep 17 00:00:00 2001 From: Yulong Ruan Date: Tue, 16 Apr 2024 18:04:32 +0800 Subject: [PATCH 3/3] fix linter Signed-off-by: Yulong Ruan --- .../public/components/workspace_creator/workspace_creator.tsx | 2 +- src/plugins/workspace/public/plugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index 176971172590..b32eec99c258 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -7,6 +7,7 @@ import React, { useCallback } from 'react'; import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { useObservable } from 'react-use'; +import { BehaviorSubject, of } from 'rxjs'; import { PublicAppInfo } from 'opensearch-dashboards/public'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; @@ -14,7 +15,6 @@ import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from ' import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants'; import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { WorkspaceClient } from '../../workspace_client'; -import { BehaviorSubject, of } from 'rxjs'; export interface WorkspaceCreatorProps { workspaceConfigurableApps$?: BehaviorSubject; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index a615b71cf4e5..05243dfa7900 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -6,6 +6,7 @@ import { BehaviorSubject, Subscription } from 'rxjs'; import React from 'react'; import { i18n } from '@osd/i18n'; +import { first } from 'rxjs/operators'; import { Plugin, CoreStart, @@ -30,7 +31,6 @@ import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_object import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; import { filterWorkspaceConfigurableApps, isAppAccessibleInWorkspace } from './utils'; -import { first } from 'rxjs/operators'; type WorkspaceAppType = ( params: AppMountParameters,