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