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`] = `
+
+
+
+
+
+
+
+
+
+ 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,
};