Skip to content

Commit

Permalink
Support to display workspace feature configured with wildcard (opense…
Browse files Browse the repository at this point in the history
…arch-project#186)

* Support to display workspace feature configured with wildcard

1. In workspace update page, support to display workspace feature
configured with wildcard.

2. Fixed an issue which generateWorkspacePermissionError return void

3. Fixed an issue which workspace route validation failed due to missing
   permission mode in schema

Signed-off-by: Yulong Ruan <ruanyl@amazon.com>

* update dependence array

Signed-off-by: Yulong Ruan <ruanyl@amazon.com>

---------

Signed-off-by: Yulong Ruan <ruanyl@amazon.com>
  • Loading branch information
ruanyl authored and Hailong-am committed Oct 9, 2023
1 parent 891a91a commit 0eb988e
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback, useState, FormEventHandler, useRef, useMemo } from 'react';
import React, { useCallback, useState, FormEventHandler, useRef, useMemo, useEffect } from 'react';
import { groupBy } from 'lodash';
import {
EuiPanel,
Expand Down Expand Up @@ -33,6 +33,7 @@ import {
AppNavLinkStatus,
ApplicationStart,
DEFAULT_APP_CATEGORIES,
MANAGEMENT_WORKSPACE_ID,
} from '../../../../../core/public';
import { useApplications } from '../../hooks';
import {
Expand All @@ -51,6 +52,7 @@ import {
WorkspacePermissionSetting,
WorkspacePermissionSettingPanel,
} from './workspace_permission_setting_panel';
import { featureMatchesConfig } from '../../utils';

interface WorkspaceFeature extends Pick<App, 'dependencies'> {
id: string;
Expand Down Expand Up @@ -128,8 +130,26 @@ export const WorkspaceForm = ({
const [icon, setIcon] = useState(defaultValues?.icon);
const [defaultVISTheme, setDefaultVISTheme] = useState(defaultValues?.defaultVISTheme);

const isEditingManagementWorkspace = defaultValues?.id === MANAGEMENT_WORKSPACE_ID;

// The matched feature id list based on original feature config,
// the feature category will be expanded to list of feature ids
const defaultFeatures = useMemo(() => {
// The original feature list, may contain feature id and category wildcard like @management, etc.
const defaultOriginalFeatures = defaultValues?.features ?? [];
return applications.filter(featureMatchesConfig(defaultOriginalFeatures)).map((app) => app.id);
}, [defaultValues?.features, applications]);

const defaultFeaturesRef = useRef(defaultFeatures);
defaultFeaturesRef.current = defaultFeatures;

useEffect(() => {
// When applications changed, reset form feature selection to original value
setSelectedFeatureIds(appendDefaultFeatureIds(defaultFeaturesRef.current));
}, [applications]);

const [selectedFeatureIds, setSelectedFeatureIds] = useState(
appendDefaultFeatureIds(defaultValues?.features || [])
appendDefaultFeatureIds(defaultFeatures)
);
const [permissionSettings, setPermissionSettings] = useState<
Array<Partial<WorkspacePermissionSetting>>
Expand Down Expand Up @@ -324,11 +344,26 @@ export const WorkspaceForm = ({
setFormErrors({ permissions: permissionErrors });
return;
}

const featureConfigChanged =
formData.features.length !== defaultFeatures.length ||
formData.features.some((feat) => !defaultFeatures.includes(feat));

if (!featureConfigChanged) {
// If feature config not changed, set workspace feature config to the original value.
// The reason why we do this is when a workspace feature is configured by wildcard,
// such as `['@management']` or `['*']`. The form value `formData.features` will be
// expanded to array of individual feature id, if the feature hasn't changed, we will
// set the feature config back to the original value so that category wildcard won't
// expanded to feature ids
formData.features = defaultValues?.features ?? [];
}

const permissions = formData.permissions.filter(isValidWorkspacePermissionSetting);
setFormErrors({});
onSubmit?.({ ...formData, name: formData.name, permissions });
},
[onSubmit]
[onSubmit, defaultFeatures, defaultValues?.features]
);

const handleNameInputChange = useCallback<Required<EuiFieldTextProps>['onChange']>((e) => {
Expand Down Expand Up @@ -397,69 +432,73 @@ export const WorkspaceForm = ({
</EuiFormRow>
</EuiPanel>
<EuiSpacer />
<EuiPanel>
<EuiTitle size="s">
<h2>Workspace features</h2>
</EuiTitle>
<EuiFlexGrid style={{ paddingLeft: 20, paddingTop: 20 }} columns={2}>
{featureOrGroups.map((featureOrGroup) => {
const features = isWorkspaceFeatureGroup(featureOrGroup) ? featureOrGroup.features : [];
const selectedIds = selectedFeatureIds.filter((id) =>
(isWorkspaceFeatureGroup(featureOrGroup)
{!isEditingManagementWorkspace && (
<EuiPanel>
<EuiTitle size="s">
<h2>Workspace features</h2>
</EuiTitle>
<EuiFlexGrid style={{ paddingLeft: 20, paddingTop: 20 }} columns={2}>
{featureOrGroups.map((featureOrGroup) => {
const features = isWorkspaceFeatureGroup(featureOrGroup)
? featureOrGroup.features
: [featureOrGroup]
).find((item) => item.id === id)
);
return (
<EuiFlexItem key={featureOrGroup.name}>
<EuiCheckbox
id={
isWorkspaceFeatureGroup(featureOrGroup)
? featureOrGroup.name
: featureOrGroup.id
}
onChange={
isWorkspaceFeatureGroup(featureOrGroup)
? handleFeatureGroupChange
: handleFeatureCheckboxChange
}
label={`${featureOrGroup.name}${
features.length > 0 ? `(${selectedIds.length}/${features.length})` : ''
}`}
checked={selectedIds.length > 0}
disabled={
!isWorkspaceFeatureGroup(featureOrGroup) &&
isDefaultCheckedFeatureId(featureOrGroup.id)
}
indeterminate={
isWorkspaceFeatureGroup(featureOrGroup) &&
selectedIds.length > 0 &&
selectedIds.length < features.length
}
/>
{isWorkspaceFeatureGroup(featureOrGroup) && (
<EuiCheckboxGroup
options={featureOrGroup.features.map((item) => ({
id: item.id,
label: item.name,
disabled: isDefaultCheckedFeatureId(item.id),
}))}
idToSelectedMap={selectedIds.reduce(
(previousValue, currentValue) => ({
...previousValue,
[currentValue]: true,
}),
{}
)}
onChange={handleFeatureChange}
style={{ marginLeft: 40 }}
: [];
const selectedIds = selectedFeatureIds.filter((id) =>
(isWorkspaceFeatureGroup(featureOrGroup)
? featureOrGroup.features
: [featureOrGroup]
).find((item) => item.id === id)
);
return (
<EuiFlexItem key={featureOrGroup.name}>
<EuiCheckbox
id={
isWorkspaceFeatureGroup(featureOrGroup)
? featureOrGroup.name
: featureOrGroup.id
}
onChange={
isWorkspaceFeatureGroup(featureOrGroup)
? handleFeatureGroupChange
: handleFeatureCheckboxChange
}
label={`${featureOrGroup.name}${
features.length > 0 ? `(${selectedIds.length}/${features.length})` : ''
}`}
checked={selectedIds.length > 0}
disabled={
!isWorkspaceFeatureGroup(featureOrGroup) &&
isDefaultCheckedFeatureId(featureOrGroup.id)
}
indeterminate={
isWorkspaceFeatureGroup(featureOrGroup) &&
selectedIds.length > 0 &&
selectedIds.length < features.length
}
/>
)}
</EuiFlexItem>
);
})}
</EuiFlexGrid>
</EuiPanel>
{isWorkspaceFeatureGroup(featureOrGroup) && (
<EuiCheckboxGroup
options={featureOrGroup.features.map((item) => ({
id: item.id,
label: item.name,
disabled: isDefaultCheckedFeatureId(item.id),
}))}
idToSelectedMap={selectedIds.reduce(
(previousValue, currentValue) => ({
...previousValue,
[currentValue]: true,
}),
{}
)}
onChange={handleFeatureChange}
style={{ marginLeft: 40 }}
/>
)}
</EuiFlexItem>
);
})}
</EuiFlexGrid>
</EuiPanel>
)}
<EuiSpacer />
<EuiPanel>
<EuiTitle size="s">
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/workspace/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { IWorkspaceDBImpl, WorkspaceRoutePermissionItem } from '../types';
export const WORKSPACES_API_BASE_URL = '/api/workspaces';

const workspacePermissionMode = schema.oneOf([
schema.literal(WorkspacePermissionMode.Write),
schema.literal(WorkspacePermissionMode.Read),
schema.literal(WorkspacePermissionMode.LibraryRead),
schema.literal(WorkspacePermissionMode.LibraryWrite),
schema.literal(WorkspacePermissionMode.Management),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@ const ALL_WORKSPACE_INNER_DATA_PERMISSION_MODES: string[] = [
];

// Can't throw unauthorized for now, the page will be refreshed if unauthorized
const generateWorkspacePermissionError = () => {
const generateWorkspacePermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
new Error(
i18n.translate('workspace.permission.invalidate', {
defaultMessage: 'Invalid workspace permission',
})
)
);
};

const generateSavedObjectsPermissionError = () =>
SavedObjectsErrorHelpers.decorateForbiddenError(
Expand Down

0 comments on commit 0eb988e

Please sign in to comment.