diff --git a/changelogs/fragments/8422.yml b/changelogs/fragments/8422.yml new file mode 100644 index 000000000000..d8afcd0a7008 --- /dev/null +++ b/changelogs/fragments/8422.yml @@ -0,0 +1,2 @@ +feat: +- Support use case populate for workspace create and list page ([#8422](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8422)) \ No newline at end of file diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index 27f0cf75aab9..bd197b5b7d3d 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -25,7 +25,13 @@ export const renderCreatorApp = ( ) => { ReactDOM.render( <OpenSearchDashboardsContextProvider services={services}> - <WorkspaceCreatorApp {...props} /> + <Router> + <Switch> + <Route> + <WorkspaceCreatorApp {...props} /> + </Route> + </Switch> + </Router> </OpenSearchDashboardsContextProvider>, element ); @@ -56,7 +62,13 @@ export const renderListApp = ( ) => { ReactDOM.render( <OpenSearchDashboardsContextProvider services={services}> - <WorkspaceListApp {...props} /> + <Router> + <Switch> + <Route> + <WorkspaceListApp {...props} /> + </Route> + </Switch> + </Router> </OpenSearchDashboardsContextProvider>, element ); diff --git a/src/plugins/workspace/public/components/utils/workspace.test.ts b/src/plugins/workspace/public/components/utils/workspace.test.ts index f1ab368fe8ce..2934ddd53cea 100644 --- a/src/plugins/workspace/public/components/utils/workspace.test.ts +++ b/src/plugins/workspace/public/components/utils/workspace.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { navigateToWorkspaceDetail } from './workspace'; +import { navigateToWorkspaceDetail, navigateToWorkspaceListWithUseCase } from './workspace'; import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; jest.mock('../../../../../core/public/utils'); @@ -62,4 +62,16 @@ describe('workspace utils', () => { expect(mockNavigateToUrl).not.toBeCalled(); }); }); + + describe('navigateToWorkspaceListWithUseCase', () => { + it('should redirect if newUrl is returned', () => { + coreStartMock.application.getUrlForApp.mockImplementation( + () => 'localhost:5601/app/workspace_list' + ); + navigateToWorkspaceListWithUseCase(coreStartMock.application, 'Search'); + expect(mockNavigateToUrl).toHaveBeenCalledWith( + 'localhost:5601/app/workspace_list#/?useCase=Search' + ); + }); + }); }); diff --git a/src/plugins/workspace/public/components/utils/workspace.ts b/src/plugins/workspace/public/components/utils/workspace.ts index e9565a07b857..7fabc73b39c7 100644 --- a/src/plugins/workspace/public/components/utils/workspace.ts +++ b/src/plugins/workspace/public/components/utils/workspace.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { WORKSPACE_DETAIL_APP_ID } from '../../../common/constants'; +import { WORKSPACE_DETAIL_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants'; import { CoreStart } from '../../../../../core/public'; import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; import { DetailTab } from '../workspace_form/constants'; @@ -23,6 +23,18 @@ export const navigateToWorkspaceDetail = ( ); }; +export const navigateToWorkspaceListWithUseCase = ( + application: Core['application'], + useCaseTitle: string +) => { + const newUrl = application.getUrlForApp(WORKSPACE_LIST_APP_ID, { absolute: true }); + if (newUrl) { + const url = new URL(newUrl); + url.hash = `/?useCase=${useCaseTitle}`; + application.navigateToUrl(url.toString()); + } +}; + export const navigateToAppWithinWorkspace = ( { application, http }: Core, workspaceId: string, 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 fd394a8bb574..c6edf7ac355d 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,6 +19,16 @@ import { DataSourceEngineType } from '../../../../data_source/common/data_source import { DataSourceConnectionType } from '../../../common/types'; import * as utils from '../../utils'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + search: '', + pathname: '', + hash: '', + state: undefined, + }), +})); + const workspaceClientCreate = jest .fn() .mockReturnValue({ result: { id: 'successResult' }, success: true }); 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 13b868c164c6..96f6c1acc88f 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useMemo } from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { BehaviorSubject } from 'rxjs'; +import { useLocation } from 'react-router-dom'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form'; import { WORKSPACE_DETAIL_APP_ID } from '../../../common/constants'; @@ -53,16 +54,28 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { onlyAllowEssentialEnabled: true, }); - const defaultSelectedUseCase = availableUseCases?.[0]; - const defaultWorkspaceFormValues: Partial<WorkspaceFormSubmitData> = { - color: euiPaletteColorBlind()[0], - ...(defaultSelectedUseCase - ? { - name: defaultSelectedUseCase.title, - features: [getUseCaseFeatureConfig(defaultSelectedUseCase.id)], - } - : {}), - }; + const location = useLocation(); + + const defaultWorkspaceFormValues = useMemo(() => { + let defaultSelectedUseCase; + const params = new URLSearchParams(location.search); + const useCaseTitle = params.get('useCase'); + if (useCaseTitle) { + defaultSelectedUseCase = + availableUseCases?.find(({ title }) => title === useCaseTitle) || availableUseCases?.[0]; + } else { + defaultSelectedUseCase = availableUseCases?.[0]; + } + return { + color: euiPaletteColorBlind()[0], + ...(defaultSelectedUseCase + ? { + name: defaultSelectedUseCase.title, + features: [getUseCaseFeatureConfig(defaultSelectedUseCase.id)], + } + : {}), + }; + }, [location.search, availableUseCases]); const handleWorkspaceFormSubmit = useCallback( async (data: WorkspaceFormSubmitData) => { 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 b30c67b37643..31b7c92abcbf 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 @@ -248,7 +248,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` data-test-subj="tableHeaderCell_description_2" role="columnheader" scope="col" - style="width: 15%;" + style="width: 18%;" > <span class="euiTableCellContent" @@ -263,28 +263,10 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </th> <th class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_permissions_3" - role="columnheader" - scope="col" - style="width: 15%;" - > - <span - class="euiTableCellContent" - > - <span - class="euiTableCellContent__text" - title="Owners" - > - Owners - </span> - </span> - </th> - <th - class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_lastUpdatedTime_4" + data-test-subj="tableHeaderCell_lastUpdatedTime_3" role="columnheader" scope="col" - style="width: 15%;" + style="width: 18%;" > <span class="euiTableCellContent" @@ -299,10 +281,10 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </th> <th class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_dataSources_5" + data-test-subj="tableHeaderCell_dataSources_4" role="columnheader" scope="col" - style="width: 15%;" + style="width: 18%;" > <span class="euiTableCellContent" @@ -417,7 +399,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -441,53 +423,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Owners - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - > - admin - - <span - class="euiBadge euiBadge--hollow euiBadge--iconRight" - > - <span - class="euiBadge__content" - > - <button - aria-label="Open workspace detail" - class="euiBadge__childButton" - data-test-subj="workspaceList-more-collaborators-badge" - title="+ 1 more" - > - + - 1 - more - </button> - <button - aria-label="Open workspace detail" - class="euiBadge__iconButton" - title="Open workspace detail" - type="button" - > - <span - class="euiBadge__icon" - color="inherit" - data-euiicon-type="popout" - /> - </button> - </span> - </span> - </div> - </td> - <td - class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -502,7 +438,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -635,7 +571,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -659,20 +595,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Owners - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - /> - </td> - <td - class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -687,7 +610,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -820,7 +743,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -842,20 +765,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" - > - <div - class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" - > - Owners - </div> - <div - class="euiTableCellContent euiTableCellContent--overflowingContent" - /> - </td> - <td - class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" @@ -870,7 +780,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` </td> <td class="euiTableRowCell" - style="width: 15%;" + style="width: 18%;" > <div class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx index eeeec9e06fb0..16131bdff272 100644 --- a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx @@ -38,6 +38,16 @@ jest.mock('../../utils', () => { }; }); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + search: '', + pathname: '', + hash: '', + state: undefined, + }), +})); + function getWrapUserDefaultWorkspaceList( workspaceList = [ { 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 62709e4fe053..f7d031b4115b 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -16,6 +16,16 @@ import { WorkspaceList } from './index'; jest.mock('../utils/workspace'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + search: '', + pathname: '', + hash: '', + state: undefined, + }), +})); + const mockNavigatorWrite = jest.fn(); jest.mock('@elastic/eui', () => { @@ -279,15 +289,4 @@ describe('WorkspaceList', () => { }); expect(navigateToWorkspaceDetail).toHaveBeenCalledTimes(1); }); - - it('should render owners badge when more than one owners', async () => { - const { getByTestId } = render(getWrapWorkspaceListInContext()); - expect(navigateToWorkspaceDetail).not.toHaveBeenCalled(); - await waitFor(() => { - const badge = getByTestId('workspaceList-more-collaborators-badge'); - expect(badge).toBeInTheDocument(); - fireEvent.click(badge); - }); - expect(navigateToWorkspaceDetail).toHaveBeenCalledTimes(1); - }); }); diff --git a/src/plugins/workspace/public/components/workspace_list/index.tsx b/src/plugins/workspace/public/components/workspace_list/index.tsx index 68b4e2acc886..65287a8ba89d 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.tsx @@ -22,17 +22,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + Query, } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject, of } from 'rxjs'; import { i18n } from '@osd/i18n'; import { isString } from 'lodash'; import { startCase } from 'lodash'; -import { - DEFAULT_NAV_GROUPS, - WorkspaceAttribute, - WorkspaceAttributeWithPermission, -} from '../../../../../core/public'; +import { useLocation } from 'react-router-dom'; +import { WorkspaceAttribute, WorkspaceAttributeWithPermission } from '../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; import { navigateToWorkspaceDetail } from '../utils/workspace'; import { DetailTab } from '../workspace_form/constants'; @@ -40,10 +38,9 @@ import { DetailTab } from '../workspace_form/constants'; import { DEFAULT_WORKSPACE, WORKSPACE_CREATE_APP_ID } from '../../../common/constants'; import { DeleteWorkspaceModal } from '../delete_workspace_modal'; -import { getFirstUseCaseOfFeatureConfigs, getDataSourcesList } from '../../utils'; +import { getDataSourcesList, extractUseCaseTitleFromFeatures } from '../../utils'; import { WorkspaceUseCase } from '../../types'; import { NavigationPublicPluginStart } from '../../../../../plugins/navigation/public'; -import { WorkspacePermissionMode } from '../../../common/constants'; import { DataSourceAttributesWithWorkspaces } from '../../types'; export interface WorkspaceListProps { @@ -112,25 +109,11 @@ export const WorkspaceListInner = ({ const [allDataSources, setAllDataSources] = useState<DataSourceAttributesWithWorkspaces[]>([]); // default workspace state const [defaultWorkspaceId, setDefaultWorkspaceId] = useState<string | undefined>(undefined); + const [query, setQuery] = useState<EuiSearchBarProps['query'] | undefined>(undefined); - const dateFormat = uiSettings?.get('dateFormat'); + const location = useLocation(); - const extractUseCaseFromFeatures = useCallback( - (features: string[]) => { - if (!features || features.length === 0) { - return ''; - } - const useCaseId = getFirstUseCaseOfFeatureConfigs(features); - const usecase = - useCaseId === DEFAULT_NAV_GROUPS.all.id - ? DEFAULT_NAV_GROUPS.all - : registeredUseCases?.find(({ id }) => id === useCaseId); - if (usecase) { - return usecase.title; - } - }, - [registeredUseCases] - ); + const dateFormat = uiSettings?.get('dateFormat'); useEffect(() => { setDefaultWorkspaceId(uiSettings?.get(DEFAULT_WORKSPACE)); @@ -149,12 +132,33 @@ export const WorkspaceListInner = ({ .map((ds) => ds.title as string); return { ...workspace, - useCase: extractUseCaseFromFeatures(workspace.features ?? []), + useCase: extractUseCaseTitleFromFeatures( + registeredUseCases ?? [], + workspace.features ?? [] + ), dataSources: associatedDataSourcesTitles, }; } ); - }, [workspaceList, extractUseCaseFromFeatures, allDataSources]); + }, [workspaceList, allDataSources, registeredUseCases]); + + const useCaseFilterOptions = useMemo(() => { + return Array.from(new Set(newWorkspaceList.map(({ useCase }) => useCase).filter(Boolean))).map( + (useCase) => ({ + value: useCase!, + name: useCase!, + }) + ); + }, [newWorkspaceList]); + + useEffect(() => { + const params = new URLSearchParams(location.search); + const useCase = params.get('useCase'); + if (useCase && useCaseFilterOptions.find((item) => item.value === useCase)) { + setQuery((Query.parse(`useCase:"${useCase}"`) as unknown) as EuiSearchBarProps['query']); + } + }, [location.search, useCaseFilterOptions]); + const workspaceCreateUrl = useMemo(() => { if (!application) { return ''; @@ -341,18 +345,15 @@ export const WorkspaceListInner = ({ box: { incremental: true, }, + query, + onChange: (args) => setQuery((args.query as unknown) as EuiSearchBarProps['query']), filters: [ { type: 'field_value_selection', field: 'useCase', name: 'Use Case', multiSelect: false, - options: Array.from( - new Set(newWorkspaceList.map(({ useCase }) => useCase).filter(Boolean)) - ).map((useCase) => ({ - value: useCase!, - name: useCase!, - })), + options: useCaseFilterOptions, }, ], toolsLeft: renderToolsLeft(), @@ -397,7 +398,7 @@ export const WorkspaceListInner = ({ name: i18n.translate('workspace.list.columns.description.title', { defaultMessage: 'Description', }), - width: '15%', + width: '18%', render: (description: string) => ( <EuiToolTip position="bottom" @@ -411,18 +412,6 @@ export const WorkspaceListInner = ({ </EuiToolTip> ), }, - { - field: 'permissions', - name: i18n.translate('workspace.list.columns.owners.title', { defaultMessage: 'Owners' }), - width: '15%', - render: ( - permissions: WorkspaceAttributeWithPermission['permissions'], - item: WorkspaceAttributeWithPermission - ) => { - const owners = permissions?.[WorkspacePermissionMode.Write]?.users ?? []; - return renderDataWithMoreBadge(owners, 1, item.id, DetailTab.Collaborators); - }, - }, { field: 'permissionMode', name: i18n.translate('workspace.list.columns.permissions.title', { @@ -451,7 +440,7 @@ export const WorkspaceListInner = ({ name: i18n.translate('workspace.list.columns.lastUpdated.title', { defaultMessage: 'Last updated', }), - width: '15%', + width: '18%', truncateText: false, render: (lastUpdatedTime: string) => { return moment(lastUpdatedTime).format(dateFormat); @@ -459,7 +448,7 @@ export const WorkspaceListInner = ({ }, { field: 'dataSources', - width: '15%', + width: '18%', name: i18n.translate('workspace.list.columns.dataSources.title', { defaultMessage: 'Data sources', }), diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 4daa59690b3f..d767b7576b50 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -22,6 +22,7 @@ import { PublicAppInfo, WorkspaceObject, WorkspaceAvailability, + DEFAULT_NAV_GROUPS, } from '../../../core/public'; import { WORKSPACE_DETAIL_APP_ID, USE_CASE_PREFIX } from '../common/constants'; @@ -430,6 +431,23 @@ export function enrichBreadcrumbsWithWorkspace( }); } +export const extractUseCaseTitleFromFeatures = ( + registeredUseCases: WorkspaceUseCase[], + features: string[] +) => { + if (!features || features.length === 0) { + return ''; + } + const useCaseId = getFirstUseCaseOfFeatureConfigs(features); + const usecase = + useCaseId === DEFAULT_NAV_GROUPS.all.id + ? DEFAULT_NAV_GROUPS.all + : registeredUseCases?.find(({ id }) => id === useCaseId); + if (usecase) { + return usecase.title; + } +}; + /** * prepend workspace or its use case to breadcrumbs * @param core CoreStart