diff --git a/changelogs/fragments/7247.yml b/changelogs/fragments/7247.yml new file mode 100644 index 000000000000..535f4c9843b0 --- /dev/null +++ b/changelogs/fragments/7247.yml @@ -0,0 +1,2 @@ +feat: +- Register workspace list card into home page ([#7247](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7247)) \ No newline at end of file diff --git a/src/core/types/workspace.ts b/src/core/types/workspace.ts index d0a0d47b2216..c00d3576d567 100644 --- a/src/core/types/workspace.ts +++ b/src/core/types/workspace.ts @@ -14,6 +14,7 @@ export interface WorkspaceAttribute { icon?: string; reserved?: boolean; uiSettings?: Record; + lastUpdatedTime?: string; } export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute { diff --git a/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap b/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap new file mode 100644 index 000000000000..35970676eb7e --- /dev/null +++ b/src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`workspace list card render normally should show workspace list card correctly 1`] = ` +
+
+
+
+

+ Workspaces +

+
+
+ + + +
+
+
+
+ +
+ + +
+
+
+
+
+
+
    +
    + +
    +

    + No Workspaces found +

    + +
    +
    + Workspaces you have recently viewed will appear here. +
    + +
    +
+ +
+
+`; diff --git a/src/plugins/workspace/public/components/service_card/index.ts b/src/plugins/workspace/public/components/service_card/index.ts new file mode 100644 index 000000000000..9bfc561f2561 --- /dev/null +++ b/src/plugins/workspace/public/components/service_card/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WorkspaceListCard } from './workspace_list_card'; diff --git a/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx b/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx new file mode 100644 index 000000000000..24d45d42e725 --- /dev/null +++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { coreMock } from '../../../../../core/public/mocks'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { WorkspaceListCard } from './workspace_list_card'; +import { recentWorkspaceManager } from '../../recent_workspace_manager'; + +describe('workspace list card render normally', () => { + const coreStart = coreMock.createStart(); + + beforeAll(() => { + const workspaceList = [ + { + id: 'ws-1', + name: 'foo', + lastUpdatedTime: new Date().toISOString(), + }, + { + id: 'ws-2', + name: 'bar', + lastUpdatedTime: new Date().toISOString(), + }, + ]; + coreStart.workspaces.workspaceList$.next(workspaceList); + }); + + it('should show workspace list card correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should show empty state if no recently viewed workspace', () => { + const { getByTestId, getByText } = render(); + expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed'); + + // empty statue for recently viewed + expect(getByText('Workspaces you have recently viewed will appear here.')).toBeInTheDocument(); + }); + + it('should show default filter as recently viewed', () => { + recentWorkspaceManager.addRecentWorkspace('foo'); + const { getByTestId, getByText } = render(); + expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed'); + + waitFor(() => { + expect(getByText('foo')).toBeInTheDocument(); + }); + }); + + it('should show updated filter correctly', () => { + const { getByTestId, getByText } = render(); + expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently viewed'); + + const filterSelector = getByTestId('workspace_filter'); + fireEvent.change(filterSelector, { target: { value: 'updated' } }); + expect(getByTestId('workspace_filter')).toHaveDisplayValue('Recently updated'); + + // workspace list + expect(getByText('foo')).toBeInTheDocument(); + expect(getByText('bar')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx new file mode 100644 index 000000000000..12b14325ce11 --- /dev/null +++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx @@ -0,0 +1,192 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { + EuiPanel, + EuiLink, + EuiDescriptionList, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiButtonIcon, + EuiSpacer, + EuiListGroup, + EuiText, + EuiTitle, + EuiToolTip, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import moment from 'moment'; +import { orderBy } from 'lodash'; +import { CoreStart, WorkspaceObject } from '../../../../../core/public'; +import { navigateToWorkspaceDetail } from '../utils/workspace'; + +import { WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID } from '../../../common/constants'; +import { recentWorkspaceManager } from '../../recent_workspace_manager'; + +const WORKSPACE_LIST_CARD_DESCRIPTION = i18n.translate('workspace.list.card.description', { + defaultMessage: + 'Workspaces are dedicated environments for organizing and collaborating on your data, dashboards, and analytics workflows. Each Workspace acts as a self-contained space with its own set of saved objects and access controls.', +}); + +const MAX_ITEM_IN_LIST = 5; + +export interface WorkspaceListCardProps { + core: CoreStart; +} + +export const WorkspaceListCard = (props: WorkspaceListCardProps) => { + const [availableWorkspaces, setAvailableWorkspaces] = useState([]); + const [filter, setFilter] = useState('viewed'); + + useEffect(() => { + const workspaceSub = props.core.workspaces.workspaceList$.subscribe((list) => { + setAvailableWorkspaces(list || []); + }); + return () => { + workspaceSub.unsubscribe(); + }; + }, [props.core]); + + const workspaceList = useMemo(() => { + const recentWorkspaces = recentWorkspaceManager.getRecentWorkspaces() || []; + if (filter === 'viewed') { + return orderBy(recentWorkspaces, ['timestamp'], ['desc']) + .filter((ws) => availableWorkspaces.some((a) => a.id === ws.id)) + .slice(0, MAX_ITEM_IN_LIST) + .map((item) => ({ + id: item.id, + name: availableWorkspaces.find((ws) => ws.id === item.id)?.name!, + time: item.timestamp, + })); + } else if (filter === 'updated') { + return orderBy(availableWorkspaces, ['lastUpdatedTime'], ['desc']) + .slice(0, MAX_ITEM_IN_LIST) + .map((ws) => ({ + id: ws.id, + name: ws.name, + time: ws.lastUpdatedTime, + })); + } + return []; + }, [filter, availableWorkspaces]); + + const handleSwitchWorkspace = (id: string) => { + const { application, http } = props.core; + if (application && http) { + navigateToWorkspaceDetail({ application, http }, id); + } + }; + + const { application } = props.core; + + const isDashboardAdmin = application.capabilities.dashboards?.isDashboardAdmin; + + return ( + + + + +

Workspaces

+
+
+ + + + + + + { + setFilter(e.target.value); + }} + options={[ + { + value: 'viewed', + text: i18n.translate('workspace.list.card.filter.viewed', { + defaultMessage: 'Recently viewed', + }), + }, + { + value: 'updated', + text: i18n.translate('workspace.list.card.filter.updated', { + defaultMessage: 'Recently updated', + }), + }, + ]} + /> + + {isDashboardAdmin && ( + + + { + application.navigateToApp(WORKSPACE_CREATE_APP_ID); + }} + /> + + + )} +
+ + + + {workspaceList && workspaceList.length === 0 ? ( + No Workspaces found

} + body={i18n.translate('workspace.list.card.empty', { + values: { + filter, + }, + defaultMessage: 'Workspaces you have recently {filter} will appear here.', + })} + /> + ) : ( + ({ + title: ( + { + handleSwitchWorkspace(workspace.id); + }} + > + {workspace.name} + + ), + description: ( + + {moment(workspace.time).fromNow()} + + ), + }))} + /> + )} +
+ + { + application.navigateToApp(WORKSPACE_LIST_APP_ID); + }} + > + View all + +
+ ); +}; diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 6ffae95d40c3..0b683fd9862f 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -30,6 +30,7 @@ describe('Workspace plugin', () => { ...coreMock.createSetup(), chrome: chromeServiceMock.createSetupContract(), }); + beforeEach(() => { WorkspaceClientMock.mockClear(); Object.values(workspaceClientMock).forEach((item) => item.mockClear()); @@ -216,6 +217,18 @@ describe('Workspace plugin', () => { expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); }); + it('#start should register workspace list card into new home page', async () => { + const startMock = coreMock.createStart(); + startMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); + const workspacePlugin = new WorkspacePlugin(); + workspacePlugin.start(startMock, mockDependencies); + expect(mockDependencies.contentManagement.registerContentProvider).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'workspace_list_card_home', + }) + ); + }); + it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => { const workspacePlugin = new WorkspacePlugin(); const setupMock = getSetupMock(); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 40f475d4a818..1be221b546ad 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -36,6 +36,7 @@ import { Services, WorkspaceUseCase } from './types'; import { WorkspaceClient } from './workspace_client'; import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_objects_management/public'; import { ManagementSetup } from '../../../plugins/management/public'; +import { ContentManagementPluginStart } from '../../../plugins/content_management/public'; import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; import { DataSourceManagementPluginSetup } from '../../../plugins/data_source_management/public'; @@ -48,7 +49,7 @@ import { import { recentWorkspaceManager } from './recent_workspace_manager'; import { toMountPoint } from '../../opensearch_dashboards_react/public'; import { UseCaseService } from './services/use_case_service'; -import { ContentManagementPluginStart } from '../../../plugins/content_management/public'; +import { WorkspaceListCard } from './components/service_card'; import { UseCaseFooter } from './components/home_get_start_card'; import { HOME_CONTENT_AREAS } from '../../home/public'; @@ -450,12 +451,33 @@ export class WorkspacePlugin ), }); + // register workspace list in home page + this.registerWorkspaceListToHome(core, contentManagement); + // register get started card in new home page this.registerGetStartedCardToNewHome(core, contentManagement); } return {}; } + private registerWorkspaceListToHome( + core: CoreStart, + contentManagement: ContentManagementPluginStart + ) { + if (contentManagement) { + contentManagement.registerContentProvider({ + id: 'workspace_list_card_home', + getContent: () => ({ + id: 'workspace_list', + kind: 'custom', + order: 0, + render: () => React.createElement(WorkspaceListCard, { core }), + }), + getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS, + }); + } + } + public stop() { this.currentWorkspaceSubscription?.unsubscribe(); this.currentWorkspaceIdSubscription?.unsubscribe(); diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 589f1d8159d2..d4bc61638744 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -18,8 +18,8 @@ import { WorkspaceObject, WorkspaceAvailability, } from '../../../core/public'; -import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; import { WorkspaceUseCase } from './types'; +import { DEFAULT_SELECTED_FEATURES_IDS } from '../common/constants'; const USE_CASE_PREFIX = 'use-case-'; diff --git a/src/plugins/workspace/server/workspace_client.ts b/src/plugins/workspace/server/workspace_client.ts index fe88436f38e5..0b7d7c8a57c1 100644 --- a/src/plugins/workspace/server/workspace_client.ts +++ b/src/plugins/workspace/server/workspace_client.ts @@ -77,6 +77,7 @@ export class WorkspaceClient implements IWorkspaceClientImpl { ): WorkspaceAttributeWithPermission { return { ...savedObject.attributes, + lastUpdatedTime: savedObject.updated_at, id: savedObject.id, permissions: savedObject.permissions, };