,
+ index?: number
+) =>
+ [
+ ...(item.type ?? []),
+ ...(item.type === WorkspacePermissionItemType.User ? [item.userId] : []),
+ ...(item.type === WorkspacePermissionItemType.Group ? [item.group] : []),
+ ...(item.modes ?? []),
+ index,
+ ].join('-');
+
+// default permission mode is read
+export const getPermissionModeId = (modes: WorkspacePermissionMode[]) => {
+ for (const key in optionIdToWorkspacePermissionModesMap) {
+ if (optionIdToWorkspacePermissionModesMap[key].every((mode) => modes?.includes(mode))) {
+ return key;
+ }
+ }
+ return PermissionModeId.Read;
+};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_bottom_bar.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_bottom_bar.tsx
new file mode 100644
index 000000000000..c55501725a52
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_bottom_bar.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiBottomBar,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import React, { useState } from 'react';
+import { ApplicationStart } from 'opensearch-dashboards/public';
+import { WorkspaceOperationType } from '../workspace_form';
+import { WorkspaceCancelModal } from './workspace_cancel_modal';
+
+interface WorkspaceBottomBarProps {
+ formId: string;
+ operationType?: WorkspaceOperationType;
+ numberOfErrors: number;
+ application: ApplicationStart;
+ numberOfUnSavedChanges?: number;
+}
+
+export const WorkspaceBottomBar = ({
+ formId,
+ operationType,
+ numberOfErrors,
+ numberOfUnSavedChanges,
+ application,
+}: WorkspaceBottomBarProps) => {
+ const [isCancelModalVisible, setIsCancelModalVisible] = useState(false);
+ const closeCancelModal = () => setIsCancelModalVisible(false);
+ const showCancelModal = () => setIsCancelModalVisible(true);
+
+ return (
+
+
+
+
+
+
+
+ {operationType === WorkspaceOperationType.Update ? (
+
+ {i18n.translate('workspace.form.bottomBar.unsavedChanges', {
+ defaultMessage: '{numberOfUnSavedChanges} Unsaved change(s)',
+ values: {
+ numberOfUnSavedChanges,
+ },
+ })}
+
+ ) : (
+
+ {i18n.translate('workspace.form.bottomBar.errors', {
+ defaultMessage: '{numberOfErrors} Error(s)',
+ values: {
+ numberOfErrors,
+ },
+ })}
+
+ )}
+
+
+
+
+
+ {i18n.translate('workspace.form.bottomBar.cancel', {
+ defaultMessage: 'Cancel',
+ })}
+
+
+ {operationType === WorkspaceOperationType.Create && (
+
+ {i18n.translate('workspace.form.bottomBar.createWorkspace', {
+ defaultMessage: 'Create workspace',
+ })}
+
+ )}
+ {operationType === WorkspaceOperationType.Update && (
+
+ {i18n.translate('workspace.form.bottomBar.saveChanges', {
+ defaultMessage: 'Save changes',
+ })}
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_cancel_modal.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_cancel_modal.tsx
new file mode 100644
index 000000000000..040e46f9ddfc
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_cancel_modal.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { i18n } from '@osd/i18n';
+import { EuiConfirmModal } from '@elastic/eui';
+import { ApplicationStart } from 'opensearch-dashboards/public';
+import { WORKSPACE_LIST_APP_ID } from '../../../common/constants';
+
+interface WorkspaceCancelModalProps {
+ visible: boolean;
+ application: ApplicationStart;
+ closeCancelModal: () => void;
+}
+
+export const WorkspaceCancelModal = ({
+ application,
+ visible,
+ closeCancelModal,
+}: WorkspaceCancelModalProps) => {
+ if (!visible) {
+ return null;
+ }
+
+ return (
+ application?.navigateToApp(WORKSPACE_LIST_APP_ID)}
+ cancelButtonText={i18n.translate('workspace.form.cancelButtonText.', {
+ defaultMessage: 'Continue editing',
+ })}
+ confirmButtonText={i18n.translate('workspace.form.confirmButtonText.', {
+ defaultMessage: 'Discard changes',
+ })}
+ buttonColor="danger"
+ defaultFocusedButton="confirm"
+ >
+ {i18n.translate('workspace.form.cancelModal.body', {
+ defaultMessage: 'This will discard all changes. Are you sure?',
+ })}
+
+ );
+};
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
new file mode 100644
index 000000000000..61181a7a749e
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx
@@ -0,0 +1,212 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import {
+ EuiText,
+ EuiFlexItem,
+ EuiCheckbox,
+ EuiCheckboxGroup,
+ EuiFlexGroup,
+ EuiCheckboxGroupProps,
+ EuiCheckboxProps,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { groupBy } from 'lodash';
+
+import {
+ AppNavLinkStatus,
+ DEFAULT_APP_CATEGORIES,
+ PublicAppInfo,
+} from '../../../../../core/public';
+
+import { WorkspaceFeature, WorkspaceFeatureGroup } from './types';
+import { isDefaultCheckedFeatureId, isWorkspaceFeatureGroup } from './utils';
+
+const libraryCategoryLabel = i18n.translate('core.ui.libraryNavList.label', {
+ defaultMessage: 'Library',
+});
+
+interface WorkspaceFeatureSelectorProps {
+ applications: PublicAppInfo[];
+ selectedFeatures: string[];
+ onChange: (newFeatures: string[]) => void;
+}
+
+export const WorkspaceFeatureSelector = ({
+ applications,
+ selectedFeatures,
+ onChange,
+}: WorkspaceFeatureSelectorProps) => {
+ const featureOrGroups = useMemo(() => {
+ const transformedApplications = applications.map((app) => {
+ if (app.category?.id === DEFAULT_APP_CATEGORIES.opensearchDashboards.id) {
+ return {
+ ...app,
+ category: {
+ ...app.category,
+ label: libraryCategoryLabel,
+ },
+ };
+ }
+ return app;
+ });
+ const category2Applications = groupBy(transformedApplications, 'category.label');
+ return Object.keys(category2Applications).reduce<
+ Array
+ >((previousValue, currentKey) => {
+ const apps = category2Applications[currentKey];
+ const features = apps
+ .filter(
+ ({ navLinkStatus, chromeless, category }) =>
+ navLinkStatus !== AppNavLinkStatus.hidden &&
+ !chromeless &&
+ category?.id !== DEFAULT_APP_CATEGORIES.management.id
+ )
+ .map(({ id, title }) => ({
+ id,
+ name: title,
+ }));
+ if (features.length === 0) {
+ return previousValue;
+ }
+ if (currentKey === 'undefined') {
+ return [...previousValue, ...features];
+ }
+ return [
+ ...previousValue,
+ {
+ name: apps[0].category?.label || '',
+ features,
+ },
+ ];
+ }, []);
+ }, [applications]);
+
+ const handleFeatureChange = useCallback(
+ (featureId) => {
+ if (!selectedFeatures.includes(featureId)) {
+ onChange([...selectedFeatures, featureId]);
+ return;
+ }
+ onChange(selectedFeatures.filter((selectedId) => selectedId !== featureId));
+ },
+ [selectedFeatures, onChange]
+ );
+
+ const handleFeatureCheckboxChange = useCallback(
+ (e) => {
+ handleFeatureChange(e.target.id);
+ },
+ [handleFeatureChange]
+ );
+
+ const handleFeatureGroupChange = useCallback(
+ (e) => {
+ const featureOrGroup = featureOrGroups.find(
+ (item) => isWorkspaceFeatureGroup(item) && item.name === e.target.id
+ );
+ if (!featureOrGroup || !isWorkspaceFeatureGroup(featureOrGroup)) {
+ return;
+ }
+ const groupFeatureIds = featureOrGroup.features.map((feature) => feature.id);
+ // setSelectedFeatureIds((previousData) => {
+ const notExistsIds = groupFeatureIds.filter((id) => !selectedFeatures.includes(id));
+ // Check all not selected features if not been selected in current group.
+ if (notExistsIds.length > 0) {
+ onChange([...selectedFeatures, ...notExistsIds]);
+ return;
+ }
+ // Need to un-check these features, if all features in group has been selected
+ onChange(selectedFeatures.filter((featureId) => !groupFeatureIds.includes(featureId)));
+ },
+ [featureOrGroups, selectedFeatures, onChange]
+ );
+
+ return (
+ <>
+ {featureOrGroups.map((featureOrGroup) => {
+ const features = isWorkspaceFeatureGroup(featureOrGroup) ? featureOrGroup.features : [];
+ const selectedIds = selectedFeatures.filter((id) =>
+ (isWorkspaceFeatureGroup(featureOrGroup)
+ ? featureOrGroup.features
+ : [featureOrGroup]
+ ).find((item) => item.id === id)
+ );
+ const featureOrGroupId = isWorkspaceFeatureGroup(featureOrGroup)
+ ? featureOrGroup.name
+ : featureOrGroup.id;
+
+ const categoryToDescription: { [key: string]: string } = {
+ [libraryCategoryLabel]: i18n.translate(
+ 'workspace.form.featureVisibility.libraryCategory.Description',
+ {
+ defaultMessage: 'Workspace-owned library items',
+ }
+ ),
+ };
+
+ return (
+
+
+
+
+ {featureOrGroup.name}
+
+ {isWorkspaceFeatureGroup(featureOrGroup) &&
+ categoryToDescription[featureOrGroup.name] && (
+ {categoryToDescription[featureOrGroup.name]}
+ )}
+
+
+
+ 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
+ }
+ data-test-subj={`workspaceForm-workspaceFeatureVisibility-${featureOrGroupId}`}
+ />
+ {isWorkspaceFeatureGroup(featureOrGroup) && (
+ ({
+ id: item.id,
+ label: item.name,
+ disabled: isDefaultCheckedFeatureId(item.id),
+ }))}
+ idToSelectedMap={selectedIds.reduce(
+ (previousValue, currentValue) => ({
+ ...previousValue,
+ [currentValue]: true,
+ }),
+ {}
+ )}
+ onChange={handleFeatureChange}
+ style={{ marginLeft: 40 }}
+ data-test-subj={`workspaceForm-workspaceFeatureVisibility-featureWithCategory-${featureOrGroupId}`}
+ />
+ )}
+
+
+ );
+ })}
+ >
+ );
+};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx
new file mode 100644
index 000000000000..ec4f2bfed3e0
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx
@@ -0,0 +1,188 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import {
+ EuiPanel,
+ EuiSpacer,
+ EuiTitle,
+ EuiForm,
+ EuiFormRow,
+ EuiFieldText,
+ EuiText,
+ EuiColorPicker,
+ EuiHorizontalRule,
+ EuiTab,
+ EuiTabs,
+} from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+import { WorkspaceBottomBar } from './workspace_bottom_bar';
+import { WorkspacePermissionSettingPanel } from './workspace_permission_setting_panel';
+import { WorkspaceFormProps } from './types';
+import { WorkspaceFormTabs } from './constants';
+import { useWorkspaceForm } from './use_workspace_form';
+import { WorkspaceFeatureSelector } from './workspace_feature_selector';
+
+export const WorkspaceForm = (props: WorkspaceFormProps) => {
+ const {
+ application,
+ defaultValues,
+ operationType,
+ permissionEnabled,
+ permissionLastAdminItemDeletable,
+ } = props;
+ const {
+ formId,
+ formData,
+ formErrors,
+ selectedTab,
+ applications,
+ numberOfErrors,
+ handleFormSubmit,
+ handleColorChange,
+ handleFeaturesChange,
+ handleNameInputChange,
+ handleTabFeatureClick,
+ setPermissionSettings,
+ handleTabPermissionClick,
+ handleDescriptionInputChange,
+ } = useWorkspaceForm(props);
+ const workspaceDetailsTitle = i18n.translate('workspace.form.workspaceDetails.title', {
+ defaultMessage: 'Workspace Details',
+ });
+ const featureVisibilityTitle = i18n.translate('workspace.form.featureVisibility.title', {
+ defaultMessage: 'Feature Visibility',
+ });
+ const usersAndPermissionsTitle = i18n.translate('workspace.form.usersAndPermissions.title', {
+ defaultMessage: 'Users & Permissions',
+ });
+
+ return (
+
+
+
+ {workspaceDetailsTitle}
+
+
+
+
+
+
+
+ Description - optional
+ >
+ }
+ helpText={i18n.translate('workspace.form.workspaceDetails.description.helpText', {
+ defaultMessage:
+ 'Valid characters are a-z, A-Z, 0-9, (), [], _ (underscore), - (hyphen) and (space).',
+ })}
+ isInvalid={!!formErrors.description}
+ error={formErrors.description}
+ >
+
+
+
+
+
+ {i18n.translate('workspace.form.workspaceDetails.color.helpText', {
+ defaultMessage: 'Accent color for your workspace',
+ })}
+
+
+
+
+
+
+
+
+
+
+ {featureVisibilityTitle}
+
+ {permissionEnabled && (
+
+ {usersAndPermissionsTitle}
+
+ )}
+
+
+ {selectedTab === WorkspaceFormTabs.FeatureVisibility && (
+
+
+ {featureVisibilityTitle}
+
+
+
+
+
+ )}
+
+ {selectedTab === WorkspaceFormTabs.UsersAndPermissions && (
+
+
+ {usersAndPermissionsTitle}
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_icon_selector.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_icon_selector.tsx
new file mode 100644
index 000000000000..06b0a224a258
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_icon_selector.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect, EuiText } from '@elastic/eui';
+
+const icons = ['Glasses', 'Search', 'Bell', 'Package'];
+
+export const WorkspaceIconSelector = ({
+ color,
+ value,
+ onChange,
+}: {
+ color?: string;
+ value?: string;
+ onChange: (value: string) => void;
+}) => {
+ const options = icons.map((item) => ({
+ value: item,
+ inputDisplay: (
+
+
+
+
+
+ {item}
+
+
+ ),
+ }));
+ return (
+ onChange(icon)}
+ />
+ );
+};
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
new file mode 100644
index 000000000000..e17f99b0d15b
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_input.tsx
@@ -0,0 +1,129 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import {
+ EuiFlexGroup,
+ EuiComboBox,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiButtonGroup,
+} from '@elastic/eui';
+import { WorkspacePermissionMode } from '../../../common/constants';
+import {
+ WorkspacePermissionItemType,
+ optionIdToWorkspacePermissionModesMap,
+ permissionModeOptions,
+} from './constants';
+import { getPermissionModeId } from './utils';
+
+export interface WorkspacePermissionSettingInputProps {
+ index: number;
+ deletable: boolean;
+ type: WorkspacePermissionItemType;
+ userId?: string;
+ group?: string;
+ modes?: WorkspacePermissionMode[];
+ onGroupOrUserIdChange: (
+ groupOrUserId:
+ | { type: WorkspacePermissionItemType.User; userId?: string }
+ | { type: WorkspacePermissionItemType.Group; group?: string },
+ index: number
+ ) => void;
+ onPermissionModesChange: (
+ WorkspacePermissionMode: WorkspacePermissionMode[],
+ index: number
+ ) => void;
+ onDelete: (index: number) => void;
+}
+
+export const WorkspacePermissionSettingInput = ({
+ index,
+ type,
+ userId,
+ group,
+ modes,
+ deletable,
+ onDelete,
+ onGroupOrUserIdChange,
+ onPermissionModesChange,
+}: WorkspacePermissionSettingInputProps) => {
+ const groupOrUserIdSelectedOptions = useMemo(
+ () => (group || userId ? [{ label: (group || userId) as string }] : []),
+ [group, userId]
+ );
+
+ const permissionModesSelectedId = useMemo(() => getPermissionModeId(modes ?? []), [modes]);
+ const handleGroupOrUserIdCreate = useCallback(
+ (groupOrUserId) => {
+ onGroupOrUserIdChange(
+ type === WorkspacePermissionItemType.Group
+ ? { type, group: groupOrUserId }
+ : { type, userId: groupOrUserId },
+ index
+ );
+ },
+ [index, type, onGroupOrUserIdChange]
+ );
+
+ const handleGroupOrUserIdChange = useCallback(
+ (options) => {
+ if (options.length === 0) {
+ onGroupOrUserIdChange({ type }, index);
+ }
+ },
+ [index, type, onGroupOrUserIdChange]
+ );
+
+ const handlePermissionModeOptionChange = useCallback(
+ (id: string) => {
+ if (optionIdToWorkspacePermissionModesMap[id]) {
+ onPermissionModesChange([...optionIdToWorkspacePermissionModesMap[id]], index);
+ }
+ },
+ [index, onPermissionModesChange]
+ );
+
+ const handleDelete = useCallback(() => {
+ onDelete(index);
+ }, [index, onDelete]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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
new file mode 100644
index 000000000000..8d2dacc4165e
--- /dev/null
+++ b/src/plugins/workspace/public/components/workspace_form/workspace_permission_setting_panel.tsx
@@ -0,0 +1,246 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useMemo, useState, useEffect } from 'react';
+import { EuiButton, EuiFormRow, EuiText, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { WorkspacePermissionSetting } from './types';
+import {
+ WorkspacePermissionItemType,
+ optionIdToWorkspacePermissionModesMap,
+ PermissionModeId,
+} from './constants';
+import {
+ WorkspacePermissionSettingInput,
+ WorkspacePermissionSettingInputProps,
+} from './workspace_permission_setting_input';
+import { generateWorkspacePermissionItemKey, getPermissionModeId } from './utils';
+
+interface WorkspacePermissionSettingPanelProps {
+ errors?: string[];
+ lastAdminItemDeletable: boolean;
+ permissionSettings: Array>;
+ onChange?: (value: Array>) => void;
+}
+
+interface UserOrGroupSectionProps
+ extends Omit {
+ title: string;
+ nonDeletableIndex: number;
+ type: WorkspacePermissionItemType;
+}
+
+const UserOrGroupSection = ({
+ type,
+ title,
+ errors,
+ onChange,
+ permissionSettings,
+ nonDeletableIndex,
+}: UserOrGroupSectionProps) => {
+ const transformedValue = useMemo(() => {
+ if (!permissionSettings) {
+ return [];
+ }
+ const result: Array> = [];
+ /**
+ * One workspace permission setting may include multi setting options,
+ * for loop the workspace permission setting array to separate it to multi rows.
+ **/
+ for (let i = 0; i < permissionSettings.length; i++) {
+ const valueItem = permissionSettings[i];
+ // Incomplete workspace permission setting don't need to separate to multi rows
+ if (
+ !valueItem.modes ||
+ !valueItem.type ||
+ (valueItem.type === 'user' && !valueItem.userId) ||
+ (valueItem.type === 'group' && !valueItem.group)
+ ) {
+ result.push(valueItem);
+ continue;
+ }
+ /**
+ * For loop the option id to workspace permission modes map,
+ * if one settings includes all permission modes in a specific option,
+ * add these permission modes to the result array.
+ */
+ for (const key in optionIdToWorkspacePermissionModesMap) {
+ if (!Object.prototype.hasOwnProperty.call(optionIdToWorkspacePermissionModesMap, key)) {
+ continue;
+ }
+ const modesForCertainPermissionId = optionIdToWorkspacePermissionModesMap[key];
+ if (modesForCertainPermissionId.every((mode) => valueItem.modes?.includes(mode))) {
+ result.push({ ...valueItem, modes: modesForCertainPermissionId });
+ }
+ }
+ }
+ return result;
+ }, [permissionSettings]);
+
+ // default permission mode is read
+ const handleAddNewOne = useCallback(() => {
+ onChange?.([
+ ...(transformedValue ?? []),
+ { type, modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read] },
+ ]);
+ }, [onChange, type, transformedValue]);
+
+ const handleDelete = useCallback(
+ (index: number) => {
+ onChange?.((transformedValue ?? []).filter((_item, itemIndex) => itemIndex !== index));
+ },
+ [onChange, transformedValue]
+ );
+
+ const handlePermissionModesChange = useCallback<
+ WorkspacePermissionSettingInputProps['onPermissionModesChange']
+ >(
+ (modes, index) => {
+ onChange?.(
+ (transformedValue ?? []).map((item, itemIndex) =>
+ index === itemIndex ? { ...item, modes } : item
+ )
+ );
+ },
+ [onChange, transformedValue]
+ );
+
+ const handleGroupOrUserIdChange = useCallback<
+ WorkspacePermissionSettingInputProps['onGroupOrUserIdChange']
+ >(
+ (userOrGroupIdWithType, index) => {
+ onChange?.(
+ (transformedValue ?? []).map((item, itemIndex) =>
+ index === itemIndex
+ ? { ...userOrGroupIdWithType, ...(item.modes ? { modes: item.modes } : {}) }
+ : item
+ )
+ );
+ },
+ [onChange, transformedValue]
+ );
+
+ // assume that group items are always deletable
+ return (
+
+
+ {title}
+
+
+ {transformedValue?.map((item, index) => (
+
+
+
+
+
+ ))}
+
+ {i18n.translate('workspace.form.permissionSettingPanel.addNew', {
+ defaultMessage: 'Add New',
+ })}
+
+
+ );
+};
+
+export const WorkspacePermissionSettingPanel = ({
+ errors,
+ onChange,
+ permissionSettings,
+ lastAdminItemDeletable,
+}: WorkspacePermissionSettingPanelProps) => {
+ const userPermissionSettings = useMemo(
+ () =>
+ permissionSettings?.filter(
+ (permissionSettingItem) => permissionSettingItem.type === WorkspacePermissionItemType.User
+ ) ?? [],
+ [permissionSettings]
+ );
+ const groupPermissionSettings = useMemo(
+ () =>
+ permissionSettings?.filter(
+ (permissionSettingItem) => permissionSettingItem.type === WorkspacePermissionItemType.Group
+ ) ?? [],
+ [permissionSettings]
+ );
+
+ const handleUserPermissionSettingsChange = useCallback(
+ (newSettings) => {
+ onChange?.([...newSettings, ...groupPermissionSettings]);
+ },
+ [groupPermissionSettings, onChange]
+ );
+
+ const handleGroupPermissionSettingsChange = useCallback(
+ (newSettings) => {
+ onChange?.([...userPermissionSettings, ...newSettings]);
+ },
+ [userPermissionSettings, onChange]
+ );
+
+ const nonDeletableIndex = useMemo(() => {
+ let userNonDeletableIndex = -1;
+ let groupNonDeletableIndex = -1;
+ const newPermissionSettings = [...userPermissionSettings, ...groupPermissionSettings];
+ if (!lastAdminItemDeletable) {
+ const adminPermissionSettings = newPermissionSettings.filter(
+ (permission) => getPermissionModeId(permission.modes ?? []) === PermissionModeId.Admin
+ );
+ if (adminPermissionSettings.length === 1) {
+ if (adminPermissionSettings[0].type === WorkspacePermissionItemType.User) {
+ userNonDeletableIndex = userPermissionSettings.findIndex(
+ (permission) => getPermissionModeId(permission.modes ?? []) === PermissionModeId.Admin
+ );
+ } else {
+ groupNonDeletableIndex = groupPermissionSettings.findIndex(
+ (permission) => getPermissionModeId(permission.modes ?? []) === PermissionModeId.Admin
+ );
+ }
+ }
+ }
+ return { userNonDeletableIndex, groupNonDeletableIndex };
+ }, [userPermissionSettings, groupPermissionSettings, lastAdminItemDeletable]);
+
+ const { userNonDeletableIndex, groupNonDeletableIndex } = nonDeletableIndex;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts
new file mode 100644
index 000000000000..e84ee46507ef
--- /dev/null
+++ b/src/plugins/workspace/public/hooks.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { ApplicationStart, PublicAppInfo } from 'opensearch-dashboards/public';
+import { useObservable } from 'react-use';
+import { useMemo } from 'react';
+
+export function useApplications(application: ApplicationStart) {
+ const applications = useObservable(application.applications$);
+ return useMemo(() => {
+ const apps: PublicAppInfo[] = [];
+ applications?.forEach((app) => {
+ apps.push(app);
+ });
+ return apps;
+ }, [applications]);
+}
diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts
index f0050879074a..5ecdc219fe96 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -23,7 +23,7 @@ describe('Workspace plugin', () => {
await workspacePlugin.setup(setupMock, {
savedObjectsManagement: savedObjectManagementSetupMock,
});
- expect(setupMock.application.register).toBeCalledTimes(2);
+ expect(setupMock.application.register).toBeCalledTimes(3);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0);
expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1);
@@ -70,7 +70,7 @@ describe('Workspace plugin', () => {
await workspacePlugin.setup(setupMock, {
savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract(),
});
- expect(setupMock.application.register).toBeCalledTimes(2);
+ expect(setupMock.application.register).toBeCalledTimes(3);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId');
expect(setupMock.getStartServices).toBeCalledTimes(1);
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index d430caabdd5c..24fb61741cc7 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -4,6 +4,7 @@
*/
import type { Subscription } from 'rxjs';
+import { i18n } from '@osd/i18n';
import { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public';
import { featureMatchesConfig } from './utils';
import {
@@ -19,6 +20,7 @@ import {
WORKSPACE_FATAL_ERROR_APP_ID,
WORKSPACE_OVERVIEW_APP_ID,
WORKSPACE_LIST_APP_ID,
+ WORKSPACE_CREATE_APP_ID,
} from '../common/constants';
import { getWorkspaceIdFromUrl } from '../../../core/public/utils';
import { renderWorkspaceMenu } from './render_workspace_menu';
@@ -161,6 +163,19 @@ export class WorkspacePlugin implements Plugin<{}, {}, {}> {
},
});
+ // create
+ core.application.register({
+ id: WORKSPACE_CREATE_APP_ID,
+ title: i18n.translate('workspace.settings.workspaceCreate', {
+ defaultMessage: 'Create Workspace',
+ }),
+ navLinkStatus: AppNavLinkStatus.hidden,
+ async mount(params: AppMountParameters) {
+ const { renderCreatorApp } = await import('./application');
+ return mountWorkspaceApp(params, renderCreatorApp);
+ },
+ });
+
// workspace fatal error
core.application.register({
id: WORKSPACE_FATAL_ERROR_APP_ID,