From d27b2f25ee0de2de14ba476a8d744ce273895668 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 20 May 2024 16:47:17 +0800 Subject: [PATCH 01/21] Add workspace use case to workspace form Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 64 +++++++++++++++++++ .../workspace_form/use_workspace_form.ts | 31 +++++++-- .../workspace_form/workspace_form.tsx | 11 ++++ .../workspace_form/workspace_use_case.scss | 8 +++ .../workspace_form/workspace_use_case.tsx | 62 ++++++++++++++++++ src/plugins/workspace/public/utils.test.ts | 6 ++ src/plugins/workspace/public/utils.ts | 36 ++++++++++- 7 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case.scss create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 17a8291b4f72..2e5b812b307e 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -79,3 +79,67 @@ export const WORKSPACE_APP_CATEGORIES: Record = Object.free order: 14000, }, }); + +export const WORKSPACE_USE_CASES = Object.freeze({ + observability: { + id: 'observability', + title: i18n.translate('workspace.usecases.observability.title', { + defaultMessage: 'Observability', + }), + description: 'Description', + features: [ + 'data-explorer', + 'dashboards', + 'visualize', + 'maps-dashboards', + 'observability-notebooks', + 'integrations', + 'alerting', + 'anomaly-detection-dashboards', + 'observability-metrics', + 'observability-traces', + 'observability-applications', + ] as string[], + }, + 'security-analytics': { + id: 'security-analytics', + title: i18n.translate('workspace.usecases.security.analytics.title', { + defaultMessage: 'Security Analytics', + }), + description: 'Description', + features: [ + 'data-explorer', + 'dashboards', + 'visualize', + 'maps-dashboards', + 'observability-notebooks', + 'integrations', + 'opensearch_security_analytics_dashboards', + ] as string[], + }, + analytics: { + id: 'analytics', + title: i18n.translate('workspace.usecases.analytics.title', { + defaultMessage: 'Analytics', + }), + description: 'Description', + features: [ + 'data-explorer', + 'dashboards', + 'visualize', + 'maps-dashboards', + 'observability-notebooks', + 'integrations', + 'alerting', + 'anomaly-detection-dashboards', + ] as string[], + }, + search: { + id: 'search', + title: i18n.translate('workspace.usecases.search.title', { + defaultMessage: 'Search', + }), + description: 'Description', + features: ['searchRelevance'] as string[], + }, +}); diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index cded7c4bcb71..cb36827860b7 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -6,7 +6,11 @@ import { useCallback, useState, FormEventHandler, useRef, useMemo, useEffect } from 'react'; import { htmlIdGenerator, EuiFieldTextProps, EuiColorPickerProps } from '@elastic/eui'; import { useApplications } from '../../hooks'; -import { featureMatchesConfig } from '../../utils'; +import { + getUseCaseFeatureConfig, + getUseCaseFromFeatureConfig, + isUseCaseFeatureConfig, +} from '../../utils'; import { WorkspaceFormTabs } from './constants'; import { WorkspaceFormProps, WorkspaceFormErrors, WorkspacePermissionSetting } from './types'; @@ -14,6 +18,8 @@ import { appendDefaultFeatureIds, getNumberOfErrors, validateWorkspaceForm } fro const workspaceHtmlIdGenerator = htmlIdGenerator(); +const isNotNull = (value: T | null): value is T => !!value; + export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: WorkspaceFormProps) => { const applications = useApplications(application); const [name, setName] = useState(defaultValues?.name); @@ -25,9 +31,8 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works // 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]); + return defaultValues?.features ?? []; + }, [defaultValues?.features]); const defaultFeaturesRef = useRef(defaultFeatures); defaultFeaturesRef.current = defaultFeatures; @@ -59,6 +64,22 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works if (!formIdRef.current) { formIdRef.current = workspaceHtmlIdGenerator(); } + const selectedUseCases = useMemo( + () => selectedFeatureIds.map(getUseCaseFromFeatureConfig).filter(isNotNull), + [selectedFeatureIds] + ); + + const handleUseCasesChange = useCallback( + (newUseCases: string[]) => { + setSelectedFeatureIds((previousFeatureIds) => { + return [ + ...previousFeatureIds.filter((featureConfig) => !isUseCaseFeatureConfig(featureConfig)), + ...newUseCases.map((useCaseItem) => getUseCaseFeatureConfig(useCaseItem)), + ]; + }); + }, + [setSelectedFeatureIds] + ); const handleFormSubmit = useCallback( (e) => { @@ -129,9 +150,11 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works selectedTab, applications, numberOfErrors, + selectedUseCases, handleFormSubmit, handleColorChange, handleFeaturesChange, + handleUseCasesChange, handleNameInputChange, handleTabFeatureClick, setPermissionSettings, 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 de841b09c60e..f77cc62844b2 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -25,6 +25,7 @@ import { WorkspaceFormTabs } from './constants'; import { useWorkspaceForm } from './use_workspace_form'; import { WorkspaceFeatureSelector } from './workspace_feature_selector'; import { WorkspacePermissionSettingPanel } from './workspace_permission_setting_panel'; +import { WorkspaceUseCase } from './workspace_use_case'; export const WorkspaceForm = (props: WorkspaceFormProps) => { const { @@ -32,6 +33,7 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { defaultValues, operationType, permissionEnabled, + workspaceConfigurableApps, permissionLastAdminItemDeletable, } = props; const { @@ -40,9 +42,11 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { formErrors, selectedTab, numberOfErrors, + selectedUseCases, handleFormSubmit, handleColorChange, handleFeaturesChange, + handleUseCasesChange, handleNameInputChange, handleTabFeatureClick, setPermissionSettings, @@ -125,6 +129,13 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { /> + Workspace Use Case}> + + diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.scss b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.scss new file mode 100644 index 000000000000..ec4f61072cff --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.workspace-use-case-item { + height: 100%; +} diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx new file mode 100644 index 000000000000..a48b24c78bb1 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { PublicAppInfo } from 'opensearch-dashboards/public'; +import { EuiCheckableCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { WORKSPACE_USE_CASES } from '../../../common/constants'; +import './workspace_use_case.scss'; + +const ALL_USE_CASES = [ + WORKSPACE_USE_CASES.observability, + WORKSPACE_USE_CASES.analytics, + WORKSPACE_USE_CASES['security-analytics'], + WORKSPACE_USE_CASES.search, +]; + +export interface WorkspaceUseCaseProps { + configurableApps?: PublicAppInfo[]; + value: string[]; + onChange: (newValue: string[]) => void; +} + +export const WorkspaceUseCase = ({ configurableApps, value, onChange }: WorkspaceUseCaseProps) => { + const availableUseCases = useMemo(() => { + if (!configurableApps) { + return []; + } + const configurableAppsId = configurableApps.map((app) => app.id); + return ALL_USE_CASES.filter((useCase) => { + return useCase.features.some((featureId) => configurableAppsId.includes(featureId)); + }); + }, [configurableApps]); + + return ( + + {availableUseCases.map(({ id, title, description }) => ( + + { + if (!value.includes(id)) { + onChange([...value, id]); + return; + } + onChange(value.filter((item) => item !== id)); + }} + > + {description} + + + ))} + + ); +}; diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index c68dc844da2e..8d1100de9d7a 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -96,6 +96,12 @@ describe('workspace utils: featureMatchesConfig', () => { true ); }); + + it('should match features inside use cases', () => { + const match = featureMatchesConfig(['use-case-observability']); + expect(match({ id: 'vis-builder' })).toBe(true); + expect(match({ id: 'not-in-use-case' })).toBe(false); + }); }); describe('workspace utils: isAppAccessibleInWorkspace', () => { diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index ba9ab5399e21..9e50485411d2 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -12,7 +12,34 @@ import { WorkspaceObject, WorkspaceAvailability, } from '../../../core/public'; -import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; +import { DEFAULT_SELECTED_FEATURES_IDS, WORKSPACE_USE_CASES } from '../common/constants'; + +const USE_CASE_PREFIX = 'use-case-'; + +export const getUseCaseFeatureConfig = (useCaseId: string) => `${USE_CASE_PREFIX}${useCaseId}`; + +export const isUseCaseFeatureConfig = (featureConfig: string) => + featureConfig.startsWith(USE_CASE_PREFIX); + +type WorkspaceUseCaseId = keyof typeof WORKSPACE_USE_CASES; + +export const getUseCaseFromFeatureConfig = (featureConfig: string) => { + if (isUseCaseFeatureConfig(featureConfig)) { + const useCaseId = featureConfig.substring(USE_CASE_PREFIX.length); + if (Object.keys(WORKSPACE_USE_CASES).includes(useCaseId)) { + return useCaseId as WorkspaceUseCaseId; + } + } + return null; +}; + +export const isFeatureIdInsideUseCase = (featureId: string, featureConfig: string) => { + const useCase = getUseCaseFromFeatureConfig(featureConfig); + if (useCase && useCase in WORKSPACE_USE_CASES) { + return WORKSPACE_USE_CASES[useCase].features.includes(featureId); + } + return false; +}; /** * Checks if a given feature matches the provided feature configuration. @@ -24,6 +51,8 @@ import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; * 4. To exclude a feature or category, prepend with `!`, e.g., `!discover` or `!@management`. * 5. The order of featureConfig array matters. From left to right, later configs override the previous ones. * For example, ['!@management', '*'] matches any feature because '*' overrides the previous setting: '!@management'. + * 6. For feature id start with use case prefix, it will read use case's features and match every passed apps. + * For example, ['user-case-observability'] matches all features under observability use case. */ export const featureMatchesConfig = (featureConfigs: string[]) => ({ id, @@ -45,6 +74,11 @@ export const featureMatchesConfig = (featureConfigs: string[]) => ({ matched = true; } + // matches any feature inside use cases + if (getUseCaseFromFeatureConfig(featureConfig)) { + matched = isFeatureIdInsideUseCase(id, featureConfig); + } + // The config starts with `@` matches a category if (category && featureConfig === `@${category.id}`) { matched = true; From 81f069a1e2dc0f2e997035869e0ec34685ecee4b Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 20 May 2024 17:40:56 +0800 Subject: [PATCH 02/21] Remove feature selector in workspace form Signed-off-by: Lin Wang --- .../public/components/workspace_form/types.ts | 10 -- .../workspace_form/use_workspace_form.ts | 71 +++------ .../components/workspace_form/utils.test.ts | 92 ------------ .../public/components/workspace_form/utils.ts | 62 +------- .../workspace_feature_selector.test.tsx | 100 ------------ .../workspace_feature_selector.tsx | 142 ------------------ .../workspace_form/workspace_form.tsx | 43 +----- 7 files changed, 25 insertions(+), 495 deletions(-) delete mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx delete mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts index 33521cc8dcb9..d8679629c48b 100644 --- a/src/plugins/workspace/public/components/workspace_form/types.ts +++ b/src/plugins/workspace/public/components/workspace_form/types.ts @@ -34,16 +34,6 @@ export interface WorkspaceFormData extends WorkspaceFormSubmitData { reserved?: boolean; } -export interface WorkspaceFeature { - id: string; - name: string; -} - -export interface WorkspaceFeatureGroup { - name: string; - features: WorkspaceFeature[]; -} - export type WorkspaceFormErrors = { [key in keyof Omit]?: string; } & { diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index cb36827860b7..ae1302ca99e3 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -26,19 +26,13 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works const [description, setDescription] = useState(defaultValues?.description); const [color, setColor] = useState(defaultValues?.color); - const [selectedTab, setSelectedTab] = useState(WorkspaceFormTabs.FeatureVisibility); - // 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. - return defaultValues?.features ?? []; - }, [defaultValues?.features]); - - const defaultFeaturesRef = useRef(defaultFeatures); - defaultFeaturesRef.current = defaultFeatures; - - const [selectedFeatureIds, setSelectedFeatureIds] = useState( - appendDefaultFeatureIds(defaultFeatures) + const [selectedTab, setSelectedTab] = useState(WorkspaceFormTabs.UsersAndPermissions); + const [featureConfigs, setFeatureConfigs] = useState( + appendDefaultFeatureIds(defaultValues?.features ?? []) + ); + const selectedUseCases = useMemo( + () => featureConfigs.map(getUseCaseFromFeatureConfig).filter(isNotNull), + [featureConfigs] ); const [permissionSettings, setPermissionSettings] = useState< Array & Partial> @@ -54,7 +48,8 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works const getFormData = () => ({ name, description, - features: selectedFeatureIds, + featureConfigs, + useCases: selectedUseCases, color, permissionSettings, }); @@ -64,21 +59,19 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works if (!formIdRef.current) { formIdRef.current = workspaceHtmlIdGenerator(); } - const selectedUseCases = useMemo( - () => selectedFeatureIds.map(getUseCaseFromFeatureConfig).filter(isNotNull), - [selectedFeatureIds] - ); const handleUseCasesChange = useCallback( (newUseCases: string[]) => { - setSelectedFeatureIds((previousFeatureIds) => { + setFeatureConfigs((previousFeatureConfigs) => { return [ - ...previousFeatureIds.filter((featureConfig) => !isUseCaseFeatureConfig(featureConfig)), + ...previousFeatureConfigs.filter( + (featureConfig) => !isUseCaseFeatureConfig(featureConfig) + ), ...newUseCases.map((useCaseItem) => getUseCaseFeatureConfig(useCaseItem)), ]; }); }, - [setSelectedFeatureIds] + [setFeatureConfigs] ); const handleFormSubmit = useCallback( @@ -91,27 +84,15 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works 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 ?? []; - } - onSubmit?.({ - ...formData, name: formData.name!, + description: formData.description, + features: formData.featureConfigs, + color: formData.color, permissionSettings: formData.permissionSettings as WorkspacePermissionSetting[], }); }, - [defaultFeatures, onSubmit, defaultValues?.features] + [onSubmit] ); const handleNameInputChange = useCallback['onChange']>((e) => { @@ -126,23 +107,10 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works setColor(text); }, []); - const handleTabFeatureClick = useCallback(() => { - setSelectedTab(WorkspaceFormTabs.FeatureVisibility); - }, []); - const handleTabPermissionClick = useCallback(() => { setSelectedTab(WorkspaceFormTabs.UsersAndPermissions); }, []); - const handleFeaturesChange = useCallback((featureIds: string[]) => { - setSelectedFeatureIds(featureIds); - }, []); - - useEffect(() => { - // When applications changed, reset form feature selection to original value - setSelectedFeatureIds(appendDefaultFeatureIds(defaultFeaturesRef.current)); - }, [applications]); - return { formId: formIdRef.current, formData: getFormData(), @@ -150,13 +118,10 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works selectedTab, applications, numberOfErrors, - selectedUseCases, handleFormSubmit, handleColorChange, - handleFeaturesChange, handleUseCasesChange, handleNameInputChange, - handleTabFeatureClick, setPermissionSettings, handleTabPermissionClick, handleDescriptionInputChange, 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 c2cf46fcf292..a4f911470ff5 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -3,106 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../../../core/public'; import { validateWorkspaceForm, - convertApplicationsToFeaturesOrGroups, convertPermissionSettingsToPermissions, convertPermissionsToPermissionSettings, } from './utils'; import { WorkspacePermissionMode } from '../../../common/constants'; import { WorkspacePermissionItemType } from './constants'; -describe('convertApplicationsToFeaturesOrGroups', () => { - it('should group same category applications in same feature group', () => { - expect( - convertApplicationsToFeaturesOrGroups([ - { - id: 'foo', - title: 'Foo', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.opensearchDashboards, - }, - { - id: 'bar', - title: 'Bar', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.opensearchDashboards, - }, - { - id: 'baz', - title: 'Baz', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.observability, - }, - ]) - ).toEqual([ - { - name: 'OpenSearch Dashboards', - features: [ - { - id: 'foo', - name: 'Foo', - }, - { - id: 'bar', - name: 'Bar', - }, - ], - }, - { - name: 'Observability', - features: [ - { - id: 'baz', - name: 'Baz', - }, - ], - }, - ]); - }); - it('should return features if application without category', () => { - expect( - convertApplicationsToFeaturesOrGroups([ - { - id: 'foo', - title: 'Foo', - navLinkStatus: AppNavLinkStatus.visible, - }, - { - id: 'baz', - title: 'Baz', - navLinkStatus: AppNavLinkStatus.visible, - category: DEFAULT_APP_CATEGORIES.observability, - }, - { - id: 'bar', - title: 'Bar', - navLinkStatus: AppNavLinkStatus.visible, - }, - ]) - ).toEqual([ - { - id: 'foo', - name: 'Foo', - }, - { - id: 'bar', - name: 'Bar', - }, - { - name: 'Observability', - features: [ - { - id: 'baz', - name: 'Baz', - }, - ], - }, - ]); - }); -}); - describe('convertPermissionSettingsToPermissions', () => { it('should return undefined if permission items not provided', () => { expect(convertPermissionSettingsToPermissions(undefined)).toBeUndefined(); diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 404314cc652a..fec59ebcc5a7 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -5,7 +5,6 @@ import { i18n } from '@osd/i18n'; -import { PublicAppInfo } from '../../../../../core/public'; import type { SavedObjectPermissions } from '../../../../../core/types'; import { DEFAULT_SELECTED_FEATURES_IDS, WorkspacePermissionMode } from '../../../common/constants'; import { @@ -14,17 +13,7 @@ import { WorkspacePermissionItemType, } from './constants'; -import { - WorkspaceFeature, - WorkspaceFeatureGroup, - WorkspaceFormErrors, - WorkspaceFormSubmitData, - WorkspacePermissionSetting, -} from './types'; - -export const isWorkspaceFeatureGroup = ( - featureOrGroup: WorkspaceFeature | WorkspaceFeatureGroup -): featureOrGroup is WorkspaceFeatureGroup => 'features' in featureOrGroup; +import { WorkspaceFormErrors, WorkspaceFormSubmitData, WorkspacePermissionSetting } from './types'; export const appendDefaultFeatureIds = (ids: string[]) => { // concat default checked ids and unique the result @@ -54,55 +43,6 @@ export const getNumberOfErrors = (formErrors: WorkspaceFormErrors) => { return numberOfErrors; }; -export const convertApplicationsToFeaturesOrGroups = ( - applications: Array< - Pick - > -) => { - const UNDEFINED = 'undefined'; - - /** - * - * Convert applications to features map, the map use category label as - * map key and group all same category applications in one array after - * transfer application to feature. - * - **/ - const categoryLabel2Features = applications.reduce<{ - [key: string]: WorkspaceFeature[]; - }>((previousValue, application) => { - const label = application.category?.label || UNDEFINED; - - return { - ...previousValue, - [label]: [...(previousValue[label] || []), { id: application.id, name: application.title }], - }; - }, {}); - - /** - * - * Iterate all keys of categoryLabel2Features map, convert map to features or groups array. - * Features with category label will be converted to feature groups. Features without "undefined" - * category label will be converted to single features. Then append them to the result array. - * - **/ - return Object.keys(categoryLabel2Features).reduce< - Array - >((previousValue, categoryLabel) => { - const features = categoryLabel2Features[categoryLabel]; - if (categoryLabel === UNDEFINED) { - return [...previousValue, ...features]; - } - return [ - ...previousValue, - { - name: categoryLabel, - features, - }, - ]; - }, []); -}; - export const isUserOrGroupPermissionSettingDuplicated = ( permissionSettings: Array>, permissionSettingToCheck: WorkspacePermissionSetting 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 deleted file mode 100644 index 313d459b6018..000000000000 --- a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.test.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { fireEvent, render } from '@testing-library/react'; -import { - WorkspaceFeatureSelector, - WorkspaceFeatureSelectorProps, -} from './workspace_feature_selector'; -import { AppNavLinkStatus, AppStatus } from '../../../../../core/public'; - -const setup = (options?: Partial) => { - const onChangeMock = jest.fn(); - const applications = [ - { - id: 'app-1', - 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( - - ); - return { - renderResult, - onChangeMock, - }; -}; - -describe('WorkspaceFeatureSelector', () => { - it('should call onChange with clicked feature', () => { - const { renderResult, onChangeMock } = setup(); - - expect(onChangeMock).not.toHaveBeenCalled(); - fireEvent.click(renderResult.getByText('App 1')); - expect(onChangeMock).toHaveBeenCalledWith(['app-1']); - }); - it('should call onChange with empty array after selected feature clicked', () => { - const { renderResult, onChangeMock } = setup({ - selectedFeatures: ['app-2'], - }); - - expect(onChangeMock).not.toHaveBeenCalled(); - fireEvent.click(renderResult.getByText('App 2')); - expect(onChangeMock).toHaveBeenCalledWith([]); - }); - it('should call onChange with features under clicked group', () => { - const { renderResult, onChangeMock } = setup(); - - expect(onChangeMock).not.toHaveBeenCalled(); - fireEvent.click( - renderResult.getByTestId('workspaceForm-workspaceFeatureVisibility-Category 1') - ); - expect(onChangeMock).toHaveBeenCalledWith(['app-1', 'app-2']); - }); - it('should call onChange without features under clicked group when group already selected', () => { - const { renderResult, onChangeMock } = setup({ - selectedFeatures: ['app-1', 'app-2', 'app-3'], - }); - - expect(onChangeMock).not.toHaveBeenCalled(); - fireEvent.click( - renderResult.getByTestId('workspaceForm-workspaceFeatureVisibility-Category 1') - ); - expect(onChangeMock).toHaveBeenCalledWith(['app-3']); - }); -}); 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 deleted file mode 100644 index 8c99e5fa6642..000000000000 --- a/src/plugins/workspace/public/components/workspace_form/workspace_feature_selector.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 { PublicAppInfo } from '../../../../../core/public'; - -import { isWorkspaceFeatureGroup, convertApplicationsToFeaturesOrGroups } from './utils'; - -export interface WorkspaceFeatureSelectorProps { - selectedFeatures: string[]; - onChange: (newFeatures: string[]) => void; - workspaceConfigurableApps?: PublicAppInfo[]; -} - -export const WorkspaceFeatureSelector = ({ - selectedFeatures, - onChange, - workspaceConfigurableApps, -}: WorkspaceFeatureSelectorProps) => { - const featuresOrGroups = useMemo( - () => convertApplicationsToFeaturesOrGroups(workspaceConfigurableApps ?? []), - [workspaceConfigurableApps] - ); - - 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 = featuresOrGroups.find( - (item) => isWorkspaceFeatureGroup(item) && item.name === e.target.id - ); - if (!featureOrGroup || !isWorkspaceFeatureGroup(featureOrGroup)) { - return; - } - const groupFeatureIds = featureOrGroup.features.map((feature) => feature.id); - 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))); - }, - [featuresOrGroups, selectedFeatures, onChange] - ); - - return ( - <> - {featuresOrGroups.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; - - return ( - - -
- - {featureOrGroup.name} - -
-
- - 0 ? ` (${selectedIds.length}/${features.length})` : '' - }`} - checked={selectedIds.length > 0} - indeterminate={ - isWorkspaceFeatureGroup(featureOrGroup) && - selectedIds.length > 0 && - selectedIds.length < features.length - } - data-test-subj={`workspaceForm-workspaceFeatureVisibility-${featureOrGroupId}`} - /> - {isWorkspaceFeatureGroup(featureOrGroup) && ( - ({ - id: item.id, - label: item.name, - }))} - 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 index f77cc62844b2..bb1df41c8fdb 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -23,7 +23,6 @@ import { WorkspaceBottomBar } from './workspace_bottom_bar'; import { WorkspaceFormProps } from './types'; import { WorkspaceFormTabs } from './constants'; import { useWorkspaceForm } from './use_workspace_form'; -import { WorkspaceFeatureSelector } from './workspace_feature_selector'; import { WorkspacePermissionSettingPanel } from './workspace_permission_setting_panel'; import { WorkspaceUseCase } from './workspace_use_case'; @@ -42,13 +41,10 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { formErrors, selectedTab, numberOfErrors, - selectedUseCases, handleFormSubmit, handleColorChange, - handleFeaturesChange, handleUseCasesChange, handleNameInputChange, - handleTabFeatureClick, setPermissionSettings, handleTabPermissionClick, handleDescriptionInputChange, @@ -56,9 +52,6 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { 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', }); @@ -132,51 +125,27 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { Workspace Use Case}> - - - {featureVisibilityTitle} - - {permissionEnabled && ( + {permissionEnabled && ( + {usersAndPermissionsTitle} - )} - - {selectedTab === WorkspaceFormTabs.FeatureVisibility && ( - - -

{featureVisibilityTitle}

-
- - - -
+
)} - {selectedTab === WorkspaceFormTabs.UsersAndPermissions && ( + {permissionEnabled && selectedTab === WorkspaceFormTabs.UsersAndPermissions && ( -

- {i18n.translate('workspace.form.usersAndPermissions.title', { - defaultMessage: 'Users & Permissions', - })} -

+

{usersAndPermissionsTitle}

Date: Mon, 20 May 2024 18:27:00 +0800 Subject: [PATCH 03/21] Show use cases in workspace list page Signed-off-by: Lin Wang --- .../public/components/workspace_list/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index b22a0fdb99fd..00340dbcd080 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -22,10 +22,11 @@ import { WorkspaceAttribute } from '../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; import { switchWorkspace, navigateToWorkspaceUpdatePage } from '../utils/workspace'; -import { WORKSPACE_CREATE_APP_ID } from '../../../common/constants'; +import { WORKSPACE_CREATE_APP_ID, WORKSPACE_USE_CASES } from '../../../common/constants'; import { cleanWorkspaceId } from '../../../../../core/public'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; +import { getUseCaseFromFeatureConfig, isUseCaseFeatureConfig } from '../../utils'; const WORKSPACE_LIST_PAGE_DESCRIPTIOIN = i18n.translate('workspace.list.description', { defaultMessage: @@ -103,9 +104,19 @@ export const WorkspaceList = () => { }, { field: 'features', - name: 'Features', + name: 'Use Cases', isExpander: true, hasActions: true, + render: (features: string[]) => { + const results: string[] = []; + features.forEach((featureConfig) => { + const useCaseId = getUseCaseFromFeatureConfig(featureConfig); + if (useCaseId) { + results.push(WORKSPACE_USE_CASES[useCaseId].title); + } + }); + return results.join(', '); + }, }, { name: 'Actions', From 2463602ac0aab0f6afc9e493ae292d2f60a1ffed Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 21 May 2024 14:51:20 +0800 Subject: [PATCH 04/21] Change direction for workspace use case selector Signed-off-by: Lin Wang --- .../components/workspace_form/workspace_form.tsx | 14 +++++++++++++- .../workspace_form/workspace_use_case.tsx | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) 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 bb1df41c8fdb..093ca9bd241b 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -122,7 +122,19 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { /> - Workspace Use Case}> +
+ + + +

+ {i18n.translate('workspace.form.workspaceUseCases.title', { + defaultMessage: 'What are you using this for?', + })} +

+
+ + + + {availableUseCases.map(({ id, title, description }) => ( Date: Tue, 21 May 2024 15:29:31 +0800 Subject: [PATCH 05/21] Modify test cases for match use case Signed-off-by: Lin Wang --- src/plugins/workspace/public/utils.test.ts | 10 ++++++---- src/plugins/workspace/public/utils.ts | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 8d1100de9d7a..28c790efc2df 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -97,10 +97,12 @@ describe('workspace utils: featureMatchesConfig', () => { ); }); - it('should match features inside use cases', () => { - const match = featureMatchesConfig(['use-case-observability']); - expect(match({ id: 'vis-builder' })).toBe(true); - expect(match({ id: 'not-in-use-case' })).toBe(false); + it('should match features include by any use cases', () => { + const match = featureMatchesConfig(['use-case-observability', 'use-case-analytics']); + expect(match({ id: 'dashboards' })).toBe(true); + expect(match({ id: 'observability-traces' })).toBe(true); + expect(match({ id: 'alerting' })).toBe(true); + expect(match({ id: 'not-in-any-use-case' })).toBe(false); }); }); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 9e50485411d2..eaf96c80a4df 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -76,7 +76,10 @@ export const featureMatchesConfig = (featureConfigs: string[]) => ({ // matches any feature inside use cases if (getUseCaseFromFeatureConfig(featureConfig)) { - matched = isFeatureIdInsideUseCase(id, featureConfig); + const isInsideUseCase = isFeatureIdInsideUseCase(id, featureConfig); + if (isInsideUseCase) { + matched = true; + } } // The config starts with `@` matches a category From a9cc376bc0d7f11799905df34509f60496cb4787 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Wed, 22 May 2024 17:20:55 +0800 Subject: [PATCH 06/21] Make use cases as a required field Signed-off-by: Lin Wang --- .../components/workspace_form/use_workspace_form.ts | 4 ++-- .../public/components/workspace_form/utils.ts | 11 ++++++++++- .../components/workspace_form/workspace_form.tsx | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index ae1302ca99e3..d59957260177 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -48,7 +48,7 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works const getFormData = () => ({ name, description, - featureConfigs, + features: featureConfigs, useCases: selectedUseCases, color, permissionSettings, @@ -87,7 +87,7 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works onSubmit?.({ name: formData.name!, description: formData.description, - features: formData.featureConfigs, + features: formData.features, color: formData.color, permissionSettings: formData.permissionSettings as WorkspacePermissionSetting[], }); diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index fec59ebcc5a7..12fea291a009 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -7,6 +7,7 @@ import { i18n } from '@osd/i18n'; import type { SavedObjectPermissions } from '../../../../../core/types'; import { DEFAULT_SELECTED_FEATURES_IDS, WorkspacePermissionMode } from '../../../common/constants'; +import { isUseCaseFeatureConfig } from '../../utils'; import { optionIdToWorkspacePermissionModesMap, PermissionModeId, @@ -40,6 +41,9 @@ export const getNumberOfErrors = (formErrors: WorkspaceFormErrors) => { if (formErrors.permissionSettings) { numberOfErrors += Object.keys(formErrors.permissionSettings).length; } + if (formErrors.features) { + numberOfErrors += 1; + } return numberOfErrors; }; @@ -183,7 +187,7 @@ export const validateWorkspaceForm = ( } ) => { const formErrors: WorkspaceFormErrors = {}; - const { name, description, permissionSettings } = formData; + const { name, description, permissionSettings, features } = formData; if (name) { if (!isValidFormTextInput(name)) { formErrors.name = i18n.translate('workspace.form.detail.name.invalid', { @@ -200,6 +204,11 @@ export const validateWorkspaceForm = ( defaultMessage: 'Invalid workspace description', }); } + if (!features || !features.some((featureConfig) => isUseCaseFeatureConfig(featureConfig))) { + formErrors.features = i18n.translate('workspace.form.features.invalid', { + defaultMessage: 'Can not be empty', + }); + } if (permissionSettings) { const permissionSettingsErrors: { [key: number]: string } = {}; for (let i = 0; i < permissionSettings.length; i++) { 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 093ca9bd241b..b69fc823670e 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -134,7 +134,7 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { - + Date: Fri, 24 May 2024 10:48:56 +0800 Subject: [PATCH 07/21] Update ui according feedbacks Signed-off-by: Lin Wang --- .../public/components/workspace_form/utils.ts | 4 +-- .../workspace_form/workspace_form.tsx | 27 ++++++++++++++----- .../workspace_form/workspace_use_case.tsx | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 12fea291a009..ce9ce8fef195 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -205,8 +205,8 @@ export const validateWorkspaceForm = ( }); } if (!features || !features.some((featureConfig) => isUseCaseFeatureConfig(featureConfig))) { - formErrors.features = i18n.translate('workspace.form.features.invalid', { - defaultMessage: 'Can not be empty', + formErrors.features = i18n.translate('workspace.form.features.empty', { + defaultMessage: 'Use case is required. Select a use case.', }); } if (permissionSettings) { 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 b69fc823670e..3a7d382f9665 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -128,18 +128,31 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => {

{i18n.translate('workspace.form.workspaceUseCases.title', { - defaultMessage: 'What are you using this for?', + defaultMessage: 'Select the team’s focus', })}

- - + + <> + + Select one or more use cases. + + + +
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx index 1c976bd0287b..a48b24c78bb1 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -35,7 +35,7 @@ export const WorkspaceUseCase = ({ configurableApps, value, onChange }: Workspac }, [configurableApps]); return ( - + {availableUseCases.map(({ id, title, description }) => ( Date: Fri, 24 May 2024 14:13:42 +0800 Subject: [PATCH 08/21] Add management feature to dashboards and visualize use cases Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 2e5b812b307e..5dc93d7e3ca7 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -99,6 +99,8 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'observability-metrics', 'observability-traces', 'observability-applications', + // Add management avoid index patterns application not found for dashboards or visualize + 'management', ] as string[], }, 'security-analytics': { @@ -115,6 +117,8 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'observability-notebooks', 'integrations', 'opensearch_security_analytics_dashboards', + // Add management avoid index patterns application not found for dashboards or visualize + 'management', ] as string[], }, analytics: { @@ -132,6 +136,8 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'integrations', 'alerting', 'anomaly-detection-dashboards', + // Add management avoid index patterns application not found for dashboards or visualize + 'management', ] as string[], }, search: { From 47ca6f7b2fad592fe99fdcf4a343a39c89263618 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 31 May 2024 11:59:43 +0800 Subject: [PATCH 09/21] Update latest feature relationships Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 5dc93d7e3ca7..8a1d84340cad 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -93,6 +93,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'visualize', 'maps-dashboards', 'observability-notebooks', + 'reports-dashboards', 'integrations', 'alerting', 'anomaly-detection-dashboards', @@ -115,6 +116,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'visualize', 'maps-dashboards', 'observability-notebooks', + 'reports-dashboards', 'integrations', 'opensearch_security_analytics_dashboards', // Add management avoid index patterns application not found for dashboards or visualize @@ -133,6 +135,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'visualize', 'maps-dashboards', 'observability-notebooks', + 'reports-dashboards', 'integrations', 'alerting', 'anomaly-detection-dashboards', @@ -146,6 +149,14 @@ export const WORKSPACE_USE_CASES = Object.freeze({ defaultMessage: 'Search', }), description: 'Description', - features: ['searchRelevance'] as string[], + features: [ + 'data-explorer', + 'dashboards', + 'visualize', + 'maps-dashboards', + 'reports-dashboards', + 'searchRelevance', + 'management', + ] as string[], }, }); From bafdc05f274d56b0f5679f1c1a0efd4ac3a84798 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 07:30:06 +0000 Subject: [PATCH 10/21] Changeset file for PR #6887 created/updated --- changelogs/fragments/6887.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/6887.yml diff --git a/changelogs/fragments/6887.yml b/changelogs/fragments/6887.yml new file mode 100644 index 000000000000..573ec79ee3ec --- /dev/null +++ b/changelogs/fragments/6887.yml @@ -0,0 +1,2 @@ +feat: +- Add use cases to workspace form ([#6887](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6887)) \ No newline at end of file From e3f2756dcdd6ad913f117d2785148946ee115f1c Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:03:10 +0000 Subject: [PATCH 11/21] Changeset file for PR #6887 created/updated --- changelogs/fragments/6887.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/6887.yml b/changelogs/fragments/6887.yml index 573ec79ee3ec..b98591b0aa4c 100644 --- a/changelogs/fragments/6887.yml +++ b/changelogs/fragments/6887.yml @@ -1,2 +1,2 @@ feat: -- Add use cases to workspace form ([#6887](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6887)) \ No newline at end of file +- [Workspace]Add use cases to workspace form ([#6887](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6887)) \ No newline at end of file From dc2f49e30132be9df7ad04b9deec3c6ebdf9c483 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 3 Jun 2024 18:39:41 +0800 Subject: [PATCH 12/21] Update test case for workspace creator and updater Signed-off-by: Lin Wang --- .../workspace_creator.test.tsx | 66 ++++++++----------- .../workspace_form/workspace_use_case.tsx | 1 + .../workspace_updater.test.tsx | 16 ++--- 3 files changed, 35 insertions(+), 48 deletions(-) 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 1682e24c3025..8df35c2317f3 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 @@ -19,11 +19,8 @@ const navigateToApp = jest.fn(); const notificationToastsAddSuccess = jest.fn(); const notificationToastsAddDanger = jest.fn(); const PublicAPPInfoMap = new Map([ - ['app1', { id: 'app1', title: 'app1' }], - ['app2', { id: 'app2', title: 'app2', category: { id: 'category1', label: 'category1' } }], - ['app3', { id: 'app3', category: { id: 'category1', label: 'category1' } }], - ['app4', { id: 'app4', category: { id: 'category2', label: 'category2' } }], - ['app5', { id: 'app5', category: { id: 'category2', label: 'category2' } }], + ['data-explorer', { id: 'data-explorer', title: 'Data Explorer' }], + ['dashboards', { id: 'dashboards', title: 'Dashboards' }], ]); const mockCoreStart = coreMock.createStart(); @@ -133,6 +130,22 @@ describe('WorkspaceCreator', () => { expect(workspaceClientCreate).not.toHaveBeenCalled(); }); + it('should not create workspace without use cases', async () => { + setHrefSpy.mockReset(); + const { getByTestId } = render( + + ); + const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); + fireEvent.input(nameInput, { + target: { value: 'test workspace name' }, + }); + expect(setHrefSpy).not.toHaveBeenCalled(); + fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); + expect(workspaceClientCreate).not.toHaveBeenCalled(); + }); + it('cancel create workspace', async () => { const { findByText, getByTestId } = render( { fireEvent.input(colorSelector, { target: { value: '#000000' }, }); + fireEvent.click(getByTestId('workspaceUseCase-observability')); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); expect(workspaceClientCreate).toHaveBeenCalledWith( expect.objectContaining({ name: 'test workspace name', color: '#000000', description: 'test workspace description', + features: expect.arrayContaining(['use-case-observability']), }), undefined ); @@ -180,37 +195,6 @@ describe('WorkspaceCreator', () => { expect(notificationToastsAddDanger).not.toHaveBeenCalled(); }); - it('create workspace with customized features', async () => { - setHrefSpy.mockReset(); - const { getByTestId } = render( - - ); - const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); - fireEvent.input(nameInput, { - target: { value: 'test workspace name' }, - }); - fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-app1')); - fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-category1')); - expect(setHrefSpy).not.toHaveBeenCalled(); - fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); - expect(workspaceClientCreate).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'test workspace name', - features: expect.arrayContaining(['app1', 'app2', 'app3']), - }), - undefined - ); - await waitFor(() => { - expect(notificationToastsAddSuccess).toHaveBeenCalled(); - }); - expect(notificationToastsAddDanger).not.toHaveBeenCalled(); - await waitFor(() => { - expect(setHrefSpy).toHaveBeenCalledWith(expect.stringMatching(/workspace_overview$/)); - }); - }); - it('should show danger toasts after create workspace failed', async () => { workspaceClientCreate.mockReturnValueOnce({ result: { id: 'failResult' }, success: false }); const { getByTestId } = render( @@ -222,6 +206,7 @@ describe('WorkspaceCreator', () => { fireEvent.input(nameInput, { target: { value: 'test workspace name' }, }); + fireEvent.click(getByTestId('workspaceUseCase-observability')); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); expect(workspaceClientCreate).toHaveBeenCalled(); await waitFor(() => { @@ -243,6 +228,7 @@ describe('WorkspaceCreator', () => { fireEvent.input(nameInput, { target: { value: 'test workspace name' }, }); + fireEvent.click(getByTestId('workspaceUseCase-observability')); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); expect(workspaceClientCreate).toHaveBeenCalled(); await waitFor(() => { @@ -252,12 +238,16 @@ describe('WorkspaceCreator', () => { }); it('create workspace with customized permissions', async () => { - const { getByTestId, getByText, getAllByText } = render(); + const { getByTestId, getAllByText } = render( + + ); const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); fireEvent.input(nameInput, { target: { value: 'test workspace name' }, }); - fireEvent.click(getByText('Users & Permissions')); + fireEvent.click(getByTestId('workspaceUseCase-observability')); fireEvent.click(getByTestId('workspaceForm-permissionSettingPanel-user-addNew')); const userIdInput = getAllByText('Select')[0]; fireEvent.click(userIdInput); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx index a48b24c78bb1..35f85e00e552 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -52,6 +52,7 @@ export const WorkspaceUseCase = ({ configurableApps, value, onChange }: Workspac } onChange(value.filter((item) => item !== id)); }} + data-test-subj={`workspaceUseCase-${id}`} > {description} 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 450dfc357db3..9129fce004ab 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 @@ -17,11 +17,8 @@ const navigateToApp = jest.fn(); const notificationToastsAddSuccess = jest.fn(); const notificationToastsAddDanger = jest.fn(); const PublicAPPInfoMap = new Map([ - ['app1', { id: 'app1', title: 'app1' }], - ['app2', { id: 'app2', title: 'app2', category: { id: 'category1', label: 'category1' } }], - ['app3', { id: 'app3', category: { id: 'category1', label: 'category1' } }], - ['app4', { id: 'app4', category: { id: 'category2', label: 'category2' } }], - ['app5', { id: 'app5', category: { id: 'category2', label: 'category2' } }], + ['data-explorer', { id: 'data-explorer', title: 'Data Explorer' }], + ['dashboards', { id: 'dashboards', title: 'Dashboards' }], ]); const createWorkspacesSetupContractMockWithValue = () => { const currentWorkspaceId$ = new BehaviorSubject('abljlsds'); @@ -29,7 +26,7 @@ const createWorkspacesSetupContractMockWithValue = () => { id: 'abljlsds', name: 'test1', description: 'test1', - features: [], + features: ['use-case-observability'], color: '', icon: '', reserved: false, @@ -190,10 +187,9 @@ describe('WorkspaceUpdater', () => { target: { value: '#000000' }, }); - fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-app1')); - fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-category1')); + fireEvent.click(getByTestId('workspaceUseCase-observability')); + fireEvent.click(getByTestId('workspaceUseCase-analytics')); - fireEvent.click(getByText('Users & Permissions')); fireEvent.click(getByTestId('workspaceForm-permissionSettingPanel-user-addNew')); const userIdInput = getAllByText('Select')[0]; fireEvent.click(userIdInput); @@ -209,7 +205,7 @@ describe('WorkspaceUpdater', () => { name: 'test workspace name', color: '#000000', description: 'test workspace description', - features: expect.arrayContaining(['app1', 'app2', 'app3']), + features: expect.arrayContaining(['use-case-analytics']), }), { read: { From a597f5967336da3c9fc848e4933d1fec295a5126 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 4 Jun 2024 11:43:01 +0800 Subject: [PATCH 13/21] Address unit test Signed-off-by: Lin Wang --- .../workspace_form/use_workspace_form.test.ts | 34 +++++++++++++++---- .../components/workspace_form/utils.test.ts | 4 +++ .../__snapshots__/index.test.tsx.snap | 4 +-- .../components/workspace_list/index.test.tsx | 3 +- .../components/workspace_list/index.tsx | 3 ++ 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts index 2bbef2d62fbd..a652a93b1921 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts @@ -35,9 +35,11 @@ describe('useWorkspaceForm', () => { act(() => { renderResult.result.current.handleFormSubmit({ preventDefault: jest.fn() }); }); - expect(renderResult.result.current.formErrors).toEqual({ - name: 'Invalid workspace name', - }); + expect(renderResult.result.current.formErrors).toEqual( + expect.objectContaining({ + name: 'Invalid workspace name', + }) + ); expect(onSubmitMock).not.toHaveBeenCalled(); }); it('should return "Invalid workspace description" and not call onSubmit when invalid description', async () => { @@ -51,15 +53,35 @@ describe('useWorkspaceForm', () => { act(() => { renderResult.result.current.handleFormSubmit({ preventDefault: jest.fn() }); }); - expect(renderResult.result.current.formErrors).toEqual({ - description: 'Invalid workspace description', + expect(renderResult.result.current.formErrors).toEqual( + expect.objectContaining({ + description: 'Invalid workspace description', + }) + ); + expect(onSubmitMock).not.toHaveBeenCalled(); + }); + it('should return "Use case is required. Select a use case." and not call onSubmit', async () => { + const { renderResult, onSubmitMock } = setup({ + id: 'foo', + name: 'test-workspace-name', + }); + expect(renderResult.result.current.formErrors).toEqual({}); + + act(() => { + renderResult.result.current.handleFormSubmit({ preventDefault: jest.fn() }); }); + expect(renderResult.result.current.formErrors).toEqual( + expect.objectContaining({ + features: 'Use case is required. Select a use case.', + }) + ); expect(onSubmitMock).not.toHaveBeenCalled(); }); it('should call onSubmit with workspace name and features', async () => { const { renderResult, onSubmitMock } = setup({ id: 'foo', name: 'test-workspace-name', + features: ['use-case-observability'], }); expect(renderResult.result.current.formErrors).toEqual({}); @@ -69,7 +91,7 @@ describe('useWorkspaceForm', () => { expect(onSubmitMock).toHaveBeenCalledWith( expect.objectContaining({ name: 'test-workspace-name', - features: ['workspace_update', 'workspace_overview'], + features: ['use-case-observability', 'workspace_update', 'workspace_overview'], }) ); }); 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 a4f911470ff5..ace2e31ed5a1 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -163,6 +163,9 @@ describe('validateWorkspaceForm', () => { 'Invalid workspace description' ); }); + it('should return error if use case is empty', () => { + expect(validateWorkspaceForm({}).features).toEqual('Use case is required. Select a use case.'); + }); it('should return error if permission setting type is invalid', () => { expect( validateWorkspaceForm({ @@ -263,6 +266,7 @@ describe('validateWorkspaceForm', () => { group: 'foo', }, ], + features: ['use-case-observability'], }) ).toEqual({}); }); 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 f90101772950..91adadd5bc5c 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 @@ -246,9 +246,9 @@ exports[`WorkspaceList should render title and table normally 1`] = ` > - Features + Use Cases 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 d75ddf0d513f..6cbf36e0c31e 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -26,8 +26,9 @@ jest.mock('../delete_workspace_modal', () => ({ function getWrapWorkspaceListInContext( workspaceList = [ - { id: 'id1', name: 'name1' }, + { id: 'id1', name: 'name1', features: [] }, { id: 'id2', name: 'name2' }, + { id: 'id3', name: 'name3', features: ['use-case-observability'] }, ] ) { const coreStartMock = coreMock.createStart(); diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 00340dbcd080..962aeb5d8448 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -108,6 +108,9 @@ export const WorkspaceList = () => { isExpander: true, hasActions: true, render: (features: string[]) => { + if (!features || features.length === 0) { + return ''; + } const results: string[] = []; features.forEach((featureConfig) => { const useCaseId = getUseCaseFromFeatureConfig(featureConfig); From f3e52ca8aae6f1102d8cd99ef946c3e2e35f24ad Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 4 Jun 2024 23:08:20 +0800 Subject: [PATCH 14/21] Add discover feature to all use case Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 8a1d84340cad..c9bc710e3f0c 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -88,7 +88,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }), description: 'Description', features: [ - 'data-explorer', + 'discover', 'dashboards', 'visualize', 'maps-dashboards', @@ -111,7 +111,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }), description: 'Description', features: [ - 'data-explorer', + 'discover', 'dashboards', 'visualize', 'maps-dashboards', @@ -130,7 +130,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }), description: 'Description', features: [ - 'data-explorer', + 'discover', 'dashboards', 'visualize', 'maps-dashboards', @@ -150,7 +150,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }), description: 'Description', features: [ - 'data-explorer', + 'discover', 'dashboards', 'visualize', 'maps-dashboards', From 8f6f45bf9aa71f252f41ba08e926ff870288b042 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Tue, 4 Jun 2024 23:17:47 +0800 Subject: [PATCH 15/21] Add missing features to security analytics Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index c9bc710e3f0c..2e19d097e42c 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -118,6 +118,8 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'observability-notebooks', 'reports-dashboards', 'integrations', + 'alerting', + 'anomaly-detection-dashboards', 'opensearch_security_analytics_dashboards', // Add management avoid index patterns application not found for dashboards or visualize 'management', From 91f5dd161ebf095ebd1c9fb87e9a17d42b809baf Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 6 Jun 2024 13:37:26 +0800 Subject: [PATCH 16/21] Address PR comments Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 16 +++-- .../workspace_use_case.test.tsx | 55 +++++++++++++++ .../workspace_form/workspace_use_case.tsx | 68 ++++++++++++++----- src/plugins/workspace/public/utils.test.ts | 7 ++ 4 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 2e19d097e42c..da21ff740241 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -86,7 +86,9 @@ export const WORKSPACE_USE_CASES = Object.freeze({ title: i18n.translate('workspace.usecases.observability.title', { defaultMessage: 'Observability', }), - description: 'Description', + description: i18n.translate('workspace.usecases.observability.description', { + defaultMessage: 'Description', + }), features: [ 'discover', 'dashboards', @@ -109,7 +111,9 @@ export const WORKSPACE_USE_CASES = Object.freeze({ title: i18n.translate('workspace.usecases.security.analytics.title', { defaultMessage: 'Security Analytics', }), - description: 'Description', + description: i18n.translate('workspace.usecases.analytics.description', { + defaultMessage: 'Description', + }), features: [ 'discover', 'dashboards', @@ -130,7 +134,9 @@ export const WORKSPACE_USE_CASES = Object.freeze({ title: i18n.translate('workspace.usecases.analytics.title', { defaultMessage: 'Analytics', }), - description: 'Description', + description: i18n.translate('workspace.usecases.analytics.description', { + defaultMessage: 'Description', + }), features: [ 'discover', 'dashboards', @@ -150,7 +156,9 @@ export const WORKSPACE_USE_CASES = Object.freeze({ title: i18n.translate('workspace.usecases.search.title', { defaultMessage: 'Search', }), - description: 'Description', + description: i18n.translate('workspace.usecases.search.description', { + defaultMessage: 'Description', + }), features: [ 'discover', 'dashboards', diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx new file mode 100644 index 000000000000..e05c6537fdc1 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { WorkspaceUseCase, WorkspaceUseCaseProps } from './workspace_use_case'; + +const setup = (options?: Partial) => { + const onChangeMock = jest.fn(); + const renderResult = render( + + ); + return { + renderResult, + onChangeMock, + }; +}; + +describe('WorkspaceUseCase', () => { + it('should render four use cases', () => { + const { renderResult } = setup(); + + expect(renderResult.getByText('Observability')).toBeInTheDocument(); + expect(renderResult.getByText('Analytics')).toBeInTheDocument(); + expect(renderResult.getByText('Security Analytics')).toBeInTheDocument(); + expect(renderResult.getByText('Search')).toBeInTheDocument(); + }); + + it('should call onChange with new added use case', () => { + const { renderResult, onChangeMock } = setup(); + + expect(onChangeMock).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByText('Observability')); + expect(onChangeMock).toHaveBeenLastCalledWith(['observability']); + }); + + it('should call onChange without removed use case', () => { + const { renderResult, onChangeMock } = setup({ value: ['observability'] }); + + expect(onChangeMock).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByText('Observability')); + expect(onChangeMock).toHaveBeenLastCalledWith([]); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx index 35f85e00e552..2ed403b967b8 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { PublicAppInfo } from 'opensearch-dashboards/public'; import { EuiCheckableCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; @@ -17,6 +17,40 @@ const ALL_USE_CASES = [ WORKSPACE_USE_CASES.search, ]; +interface WorkspaceUseCaseCardProps { + id: string; + title: string; + checked: boolean; + description: string; + onChange: (id: string) => void; +} + +const WorkspaceUseCaseCard = ({ + id, + title, + description, + checked, + onChange, +}: WorkspaceUseCaseCardProps) => { + const handleChange = useCallback(() => { + onChange(id); + }, [id, onChange]); + return ( + + {description} + + ); +}; + export interface WorkspaceUseCaseProps { configurableApps?: PublicAppInfo[]; value: string[]; @@ -34,28 +68,28 @@ export const WorkspaceUseCase = ({ configurableApps, value, onChange }: Workspac }); }, [configurableApps]); + const handleCardChange = useCallback( + (id: string) => { + if (!value.includes(id)) { + onChange([...value, id]); + return; + } + onChange(value.filter((item) => item !== id)); + }, + [value, onChange] + ); + return ( {availableUseCases.map(({ id, title, description }) => ( - { - if (!value.includes(id)) { - onChange([...value, id]); - return; - } - onChange(value.filter((item) => item !== id)); - }} - data-test-subj={`workspaceUseCase-${id}`} - > - {description} - + onChange={handleCardChange} + /> ))} diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 28c790efc2df..70eb91cfbdda 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -8,6 +8,7 @@ import { featureMatchesConfig, filterWorkspaceConfigurableApps, isAppAccessibleInWorkspace, + isFeatureIdInsideUseCase, } from './utils'; import { WorkspaceAvailability } from '../../../core/public'; @@ -269,3 +270,9 @@ describe('workspace utils: filterWorkspaceConfigurableApps', () => { expect(filteredApps[1].id).toEqual('management'); }); }); + +describe('workspace utils: isFeatureIdInsideUseCase', () => { + it('should return false for invalid use case', () => { + expect(isFeatureIdInsideUseCase('discover', 'use-case-invalid')).toBe(false); + }); +}); From 1565a012ad88f8b755ed036983255713c59300ee Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 6 Jun 2024 16:09:35 +0800 Subject: [PATCH 17/21] Add comment for workspace use cases map Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index da21ff740241..3ad3ded2733f 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -79,7 +79,14 @@ export const WORKSPACE_APP_CATEGORIES: Record = Object.free order: 14000, }, }); - +/** + * + * This is a temp solution to store relationships between use cases and features. + * The relationship should be provided by plugin itself. The workspace plugin should + * provide some method to register single feature to the use case map instead of + * store a static map in workspace. + * + */ export const WORKSPACE_USE_CASES = Object.freeze({ observability: { id: 'observability', @@ -166,6 +173,7 @@ export const WORKSPACE_USE_CASES = Object.freeze({ 'maps-dashboards', 'reports-dashboards', 'searchRelevance', + // Add management avoid index patterns application not found for dashboards or visualize 'management', ] as string[], }, From 55e9839b1e3f1552bf7225513f0bfc28c8a48ab4 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 6 Jun 2024 22:01:06 +0800 Subject: [PATCH 18/21] Update use case UI Signed-off-by: Lin Wang --- src/plugins/workspace/common/constants.ts | 28 ++++++++++-------- .../workspace_creator/workspace_creator.tsx | 2 +- .../workspace_form/workspace_form.tsx | 29 ++++++++----------- .../workspace_form/workspace_use_case.tsx | 8 +++-- .../components/workspace_list/index.tsx | 2 +- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 3ad3ded2733f..4380ce40a10b 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -90,11 +90,12 @@ export const WORKSPACE_APP_CATEGORIES: Record = Object.free export const WORKSPACE_USE_CASES = Object.freeze({ observability: { id: 'observability', - title: i18n.translate('workspace.usecases.observability.title', { + title: i18n.translate('workspace.usecase.observability.title', { defaultMessage: 'Observability', }), - description: i18n.translate('workspace.usecases.observability.description', { - defaultMessage: 'Description', + description: i18n.translate('workspace.usecase.observability.description', { + defaultMessage: + 'Gain visibility into system health, performance, and reliability through monitoring and analysis of logs, metrics, and traces.', }), features: [ 'discover', @@ -115,11 +116,12 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }, 'security-analytics': { id: 'security-analytics', - title: i18n.translate('workspace.usecases.security.analytics.title', { + title: i18n.translate('workspace.usecase.security.analytics.title', { defaultMessage: 'Security Analytics', }), - description: i18n.translate('workspace.usecases.analytics.description', { - defaultMessage: 'Description', + description: i18n.translate('workspace.usecase.analytics.description', { + defaultMessage: + 'Detect and investigate potential security threats and vulnerabilities across your systems and data.', }), features: [ 'discover', @@ -138,11 +140,12 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }, analytics: { id: 'analytics', - title: i18n.translate('workspace.usecases.analytics.title', { + title: i18n.translate('workspace.usecase.analytics.title', { defaultMessage: 'Analytics', }), - description: i18n.translate('workspace.usecases.analytics.description', { - defaultMessage: 'Description', + description: i18n.translate('workspace.usecase.analytics.description', { + defaultMessage: + 'Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.', }), features: [ 'discover', @@ -160,11 +163,12 @@ export const WORKSPACE_USE_CASES = Object.freeze({ }, search: { id: 'search', - title: i18n.translate('workspace.usecases.search.title', { + title: i18n.translate('workspace.usecase.search.title', { defaultMessage: 'Search', }), - description: i18n.translate('workspace.usecases.search.description', { - defaultMessage: 'Description', + description: i18n.translate('workspace.usecase.search.description', { + defaultMessage: + "Quickly find and explore relevant information across your organization's data sources.", }), features: [ 'discover', 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 11d411f6e0d2..61905572f628 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -78,7 +78,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { return ( - + { handleDescriptionInputChange, } = useWorkspaceForm(props); const workspaceDetailsTitle = i18n.translate('workspace.form.workspaceDetails.title', { - defaultMessage: 'Workspace Details', + defaultMessage: 'Enter Details', }); const usersAndPermissionsTitle = i18n.translate('workspace.form.usersAndPermissions.title', { defaultMessage: 'Users & Permissions', @@ -62,7 +62,6 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => {

{workspaceDetailsTitle}

- { onChange={handleNameInputChange} readOnly={!!defaultValues?.reserved} data-test-subj="workspaceForm-workspaceDetails-nameInputText" + placeholder={i18n.translate('workspace.form.workspaceDetails.name.placeholder', { + defaultMessage: 'Enter a name', + })} /> {

- {i18n.translate('workspace.form.workspaceUseCases.title', { - defaultMessage: 'Select the team’s focus', + {i18n.translate('workspace.form.workspaceUseCase.title', { + defaultMessage: 'Choose one or more focus areas', })}

- - <> - - Select one or more use cases. - - - - +
diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx index 2ed403b967b8..8e47d9eafc77 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -5,15 +5,15 @@ import React, { useMemo, useCallback } from 'react'; import { PublicAppInfo } from 'opensearch-dashboards/public'; -import { EuiCheckableCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCheckableCard, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { WORKSPACE_USE_CASES } from '../../../common/constants'; import './workspace_use_case.scss'; const ALL_USE_CASES = [ WORKSPACE_USE_CASES.observability, - WORKSPACE_USE_CASES.analytics, WORKSPACE_USE_CASES['security-analytics'], + WORKSPACE_USE_CASES.analytics, WORKSPACE_USE_CASES.search, ]; @@ -46,7 +46,9 @@ const WorkspaceUseCaseCard = ({ onChange={handleChange} data-test-subj={`workspaceUseCase-${id}`} > - {description} + + {description} + ); }; diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 962aeb5d8448..34027b87de5b 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -104,7 +104,7 @@ export const WorkspaceList = () => { }, { field: 'features', - name: 'Use Cases', + name: 'Use case', isExpander: true, hasActions: true, render: (features: string[]) => { From 9cfa43624fb4d69c0939a207fa55441be47c155a Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Thu, 6 Jun 2024 22:07:03 +0800 Subject: [PATCH 19/21] Remove the permissions tab Signed-off-by: Lin Wang --- .../components/workspace_form/constants.ts | 6 ----- .../workspace_form/use_workspace_form.ts | 11 ++------ .../workspace_form/workspace_form.tsx | 25 ++++--------------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index 693f0cdce141..073477b2ad89 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -11,12 +11,6 @@ export enum WorkspaceOperationType { Update = 'update', } -export enum WorkspaceFormTabs { - NotSelected, - FeatureVisibility, - UsersAndPermissions, -} - export enum WorkspacePermissionItemType { User = 'user', Group = 'group', diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index d59957260177..6b6c7ffb9bfb 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useState, FormEventHandler, useRef, useMemo, useEffect } from 'react'; +import { useCallback, useState, FormEventHandler, useRef, useMemo } from 'react'; import { htmlIdGenerator, EuiFieldTextProps, EuiColorPickerProps } from '@elastic/eui'; + import { useApplications } from '../../hooks'; import { getUseCaseFeatureConfig, @@ -12,7 +13,6 @@ import { isUseCaseFeatureConfig, } from '../../utils'; -import { WorkspaceFormTabs } from './constants'; import { WorkspaceFormProps, WorkspaceFormErrors, WorkspacePermissionSetting } from './types'; import { appendDefaultFeatureIds, getNumberOfErrors, validateWorkspaceForm } from './utils'; @@ -26,7 +26,6 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works const [description, setDescription] = useState(defaultValues?.description); const [color, setColor] = useState(defaultValues?.color); - const [selectedTab, setSelectedTab] = useState(WorkspaceFormTabs.UsersAndPermissions); const [featureConfigs, setFeatureConfigs] = useState( appendDefaultFeatureIds(defaultValues?.features ?? []) ); @@ -107,15 +106,10 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works setColor(text); }, []); - const handleTabPermissionClick = useCallback(() => { - setSelectedTab(WorkspaceFormTabs.UsersAndPermissions); - }, []); - return { formId: formIdRef.current, formData: getFormData(), formErrors, - selectedTab, applications, numberOfErrors, handleFormSubmit, @@ -123,7 +117,6 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works handleUseCasesChange, handleNameInputChange, setPermissionSettings, - handleTabPermissionClick, handleDescriptionInputChange, }; }; 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 6e9c60978028..63611d2a04ea 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form.tsx @@ -14,8 +14,6 @@ import { EuiText, EuiColorPicker, EuiHorizontalRule, - EuiTab, - EuiTabs, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; @@ -39,22 +37,17 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => { formId, formData, formErrors, - selectedTab, numberOfErrors, handleFormSubmit, handleColorChange, handleUseCasesChange, handleNameInputChange, setPermissionSettings, - handleTabPermissionClick, handleDescriptionInputChange, } = useWorkspaceForm(props); const workspaceDetailsTitle = i18n.translate('workspace.form.workspaceDetails.title', { defaultMessage: 'Enter Details', }); - const usersAndPermissionsTitle = i18n.translate('workspace.form.usersAndPermissions.title', { - defaultMessage: 'Users & Permissions', - }); return ( @@ -151,23 +144,15 @@ export const WorkspaceForm = (props: WorkspaceFormProps) => {
- {permissionEnabled && ( - - - {usersAndPermissionsTitle} - - - )} - {permissionEnabled && selectedTab === WorkspaceFormTabs.UsersAndPermissions && ( -

{usersAndPermissionsTitle}

+

+ {i18n.translate('workspace.form.usersAndPermissions.title', { + defaultMessage: 'Manage access and permissions', + })} +

- Date: Thu, 6 Jun 2024 22:09:37 +0800 Subject: [PATCH 20/21] Update breadcrum to Create a workspace Signed-off-by: Lin Wang --- .../workspace/public/components/workspace_creator_app.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_app.tsx b/src/plugins/workspace/public/components/workspace_creator_app.tsx index e384f5d5bfed..fcd7dc2c96a9 100644 --- a/src/plugins/workspace/public/components/workspace_creator_app.tsx +++ b/src/plugins/workspace/public/components/workspace_creator_app.tsx @@ -22,7 +22,7 @@ export const WorkspaceCreatorApp = (props: WorkspaceCreatorProps) => { chrome?.setBreadcrumbs([ { text: i18n.translate('workspace.workspaceCreateTitle', { - defaultMessage: 'Create workspace', + defaultMessage: 'Create a workspace', }), }, ]); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 8c6bd2ddc0e6..969459e188ee 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -230,7 +230,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> core.application.register({ id: WORKSPACE_CREATE_APP_ID, title: i18n.translate('workspace.settings.workspaceCreate', { - defaultMessage: 'Create Workspace', + defaultMessage: 'Create a workspace', }), navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { From 4caf21301fa374f6658cbceffd8195764d98f763 Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Fri, 7 Jun 2024 09:41:39 +0800 Subject: [PATCH 21/21] Address ut failed Signed-off-by: Lin Wang --- .../workspace_list/__snapshots__/index.test.tsx.snap | 4 ++-- .../components/workspace_overview/workspace_overview.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 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 91adadd5bc5c..375e320e6305 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 @@ -246,9 +246,9 @@ exports[`WorkspaceList should render title and table normally 1`] = ` > - Use Cases + Use case diff --git a/src/plugins/workspace/public/components/workspace_overview/workspace_overview.test.tsx b/src/plugins/workspace/public/components/workspace_overview/workspace_overview.test.tsx index 64ff49fcbdd6..8a87510f0d9e 100644 --- a/src/plugins/workspace/public/components/workspace_overview/workspace_overview.test.tsx +++ b/src/plugins/workspace/public/components/workspace_overview/workspace_overview.test.tsx @@ -207,7 +207,7 @@ describe('WorkspaceOverview', () => { const workspaceService = createWorkspacesSetupContractMockWithValue(workspaceObject); const { getByText } = render(WorkspaceOverviewPage({ workspacesService: workspaceService })); fireEvent.click(getByText('Settings')); - expect(screen.queryByText('Workspace Details')).not.toBeNull(); + expect(screen.queryByText('Enter Details')).not.toBeNull(); // title is hidden expect(screen.queryByText('Update Workspace')).toBeNull(); });