From 2e400e4850c084b43c575615aef3bc40fbdedd11 Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Thu, 18 Jul 2024 11:13:15 +0800
Subject: [PATCH] workspace list card on home
Signed-off-by: Hailong Cui
---
src/core/public/index.ts | 2 +
src/core/types/workspace.ts | 1 +
src/plugins/home/public/index.ts | 2 +
src/plugins/workspace/common/constants.ts | 2 +
.../workspace/opensearch_dashboards.json | 4 +-
.../workspace_list_card.test.tsx.snap | 121 +++++++++
.../public/components/service_card/index.ts | 6 +
.../service_card/workspace_list_card.test.tsx | 66 +++++
.../service_card/workspace_list_card.tsx | 239 ++++++++++++++++++
src/plugins/workspace/public/plugin.ts | 37 ++-
src/plugins/workspace/public/utils.ts | 2 +-
.../workspace/server/workspace_client.ts | 1 +
12 files changed, 478 insertions(+), 5 deletions(-)
create mode 100644 src/plugins/workspace/public/components/service_card/__snapshots__/workspace_list_card.test.tsx.snap
create mode 100644 src/plugins/workspace/public/components/service_card/index.ts
create mode 100644 src/plugins/workspace/public/components/service_card/workspace_list_card.test.tsx
create mode 100644 src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 8c387361a9ca..002e1df37846 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -76,6 +76,7 @@ import {
PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
+ PersistedLog,
} from './chrome';
import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors';
import { HttpSetup, HttpStart } from './http';
@@ -379,6 +380,7 @@ export {
PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
+ PersistedLog,
};
export { __osdBootstrap__ } from './osd_bootstrap';
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/home/public/index.ts b/src/plugins/home/public/index.ts
index 58ad10cdf04b..d252a31a0977 100644
--- a/src/plugins/home/public/index.ts
+++ b/src/plugins/home/public/index.ts
@@ -53,3 +53,5 @@ import { HomePublicPlugin } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new HomePublicPlugin(initializerContext);
+
+export { HOME_PAGE_ID, HOME_CONTENT_AREAS } from '../common/constants';
diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts
index 9fef78152d8d..6cb0a00ed72a 100644
--- a/src/plugins/workspace/common/constants.ts
+++ b/src/plugins/workspace/common/constants.ts
@@ -185,3 +185,5 @@ export const WORKSPACE_USE_CASES = Object.freeze({
export const MAX_WORKSPACE_PICKER_NUM = 3;
export const RECENT_WORKSPACES_KEY = 'recentWorkspaces';
export const CURRENT_USER_PLACEHOLDER = '%me%';
+export const MAX_WORKSPACE_NAME_LENGTH = 25;
+export const RECENT_WORKSPACES_KEY = 'recentWorkspaces';
diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json
index 2e9377b3bda9..5838d070264f 100644
--- a/src/plugins/workspace/opensearch_dashboards.json
+++ b/src/plugins/workspace/opensearch_dashboards.json
@@ -7,6 +7,6 @@
"savedObjects",
"opensearchDashboardsReact"
],
- "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement"],
- "requiredBundles": ["opensearchDashboardsReact"]
+ "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"],
+ "requiredBundles": ["opensearchDashboardsReact","home"]
}
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..ca48a73e7a40
--- /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..caa918b9f4a1
--- /dev/null
+++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
@@ -0,0 +1,239 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } 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 { Subscription } from 'rxjs';
+import moment from 'moment';
+import _ 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 { WorkspaceEntry, recentWorkspaceManager } from '../../recent_workspace_manager';
+
+const WORKSPACE_LIST_CARD_DESCRIPTIOIN = i18n.translate('workspace.list.card.descriptionh', {
+ 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 interface WorkspaceListItem {
+ id: string;
+ name: string;
+ time?: string | number;
+}
+
+export interface WorkspaceListCardState {
+ availiableWorkspaces: WorkspaceObject[];
+ filter: string;
+ workspaceList: WorkspaceListItem[];
+ recentWorkspaces: WorkspaceEntry[];
+}
+
+export class WorkspaceListCard extends Component {
+ private workspaceSub?: Subscription;
+ constructor(props: WorkspaceListCardProps) {
+ super(props);
+ this.state = {
+ availiableWorkspaces: [],
+ recentWorkspaces: [],
+ workspaceList: [],
+ filter: 'viewed',
+ };
+ }
+
+ componentDidMount() {
+ this.setState({
+ recentWorkspaces: recentWorkspaceManager.getRecentWorkspaces() || [],
+ });
+ this.workspaceSub = this.props.core.workspaces.workspaceList$.subscribe((list) => {
+ this.setState({
+ availiableWorkspaces: list || [],
+ });
+ });
+ this.loadWorkspaceListItems();
+ }
+
+ componentDidUpdate(
+ prevProps: Readonly,
+ prevState: Readonly
+ ): void {
+ if (
+ !_.isEqual(prevState.filter, this.state.filter) ||
+ !_.isEqual(prevState.availiableWorkspaces, this.state.availiableWorkspaces) ||
+ !_.isEqual(prevState.recentWorkspaces, this.state.recentWorkspaces)
+ ) {
+ this.loadWorkspaceListItems();
+ }
+ }
+
+ private loadWorkspaceListItems() {
+ if (this.state.filter === 'viewed') {
+ this.setState({
+ workspaceList: _.orderBy(this.state.recentWorkspaces, ['timestamp'], ['desc'])
+ .filter((ws) => this.state.availiableWorkspaces.some((a) => a.id === ws.id))
+ .slice(0, MAX_ITEM_IN_LIST)
+ .map((item) => ({
+ id: item.id,
+ name: this.state.availiableWorkspaces.find((ws) => ws.id === item.id)?.name!,
+ time: item.timestamp,
+ })),
+ });
+ } else if (this.state.filter === 'updated') {
+ this.setState({
+ workspaceList: _.orderBy(this.state.availiableWorkspaces, ['lastUpdatedTime'], ['desc'])
+ .slice(0, MAX_ITEM_IN_LIST)
+ .map((ws) => ({
+ id: ws.id,
+ name: ws.name,
+ time: ws.lastUpdatedTime,
+ })),
+ });
+ }
+ }
+
+ componentWillUnmount() {
+ this.workspaceSub?.unsubscribe();
+ }
+
+ private handleSwitchWorkspace = (id: string) => {
+ const { application, http } = this.props.core;
+ if (application && http) {
+ navigateToWorkspaceDetail({ application, http }, id);
+ }
+ };
+
+ render() {
+ const workspaceList = this.state.workspaceList;
+ const { application } = this.props.core;
+
+ const isDashboardAdmin = application.capabilities.dashboards?.isDashboardAdmin;
+
+ return (
+
+
+
+
+ Workspaces
+
+
+
+
+
+
+
+
+ {
+ this.setState({ filter: 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: this.state.filter,
+ },
+ defaultMessage: 'Workspaces you have recently {filter} will appear here.',
+ })}
+ />
+ ) : (
+ ({
+ title: (
+ {
+ this.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.ts b/src/plugins/workspace/public/plugin.ts
index 104db7d9b91f..666f8cdde674 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -34,6 +34,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';
@@ -46,6 +47,9 @@ import {
import { recentWorkspaceManager } from './recent_workspace_manager';
import { toMountPoint } from '../../opensearch_dashboards_react/public';
import { UseCaseService } from './services/use_case_service';
+import { recentWorkspaceManager } from './recent_workspace_manager';
+import { WorkspaceListCard } from './components/service_card';
+import { HOME_CONTENT_AREAS, HOME_PAGE_ID } from '../../home/public';
type WorkspaceAppType = (
params: AppMountParameters,
@@ -59,7 +63,12 @@ interface WorkspacePluginSetupDeps {
dataSourceManagement?: DataSourceManagementPluginSetup;
}
-export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> {
+interface WorkspacePluginStartDeps {
+ contentManagement: ContentManagementPluginStart;
+}
+
+export class WorkspacePlugin
+ implements Plugin<{}, {}, WorkspacePluginSetupDeps, WorkspacePluginStartDeps> {
private coreStart?: CoreStart;
private currentWorkspaceSubscription?: Subscription;
private breadcrumbsSubscription?: Subscription;
@@ -267,6 +276,9 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
// Add workspace id to recent workspaces.
recentWorkspaceManager.addRecentWorkspace(workspaceId);
})();
+
+ // Add workspace id to recent workspaces.
+ recentWorkspaceManager.addRecentWorkspace(workspaceId);
}
}
@@ -372,7 +384,7 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
return {};
}
- public start(core: CoreStart) {
+ public start(core: CoreStart, { contentManagement }: WorkspacePluginStartDeps) {
this.coreStart = core;
this.currentWorkspaceIdSubscription = this._changeSavedObjectCurrentWorkspace();
@@ -407,9 +419,30 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps>
});
}
+ // register workspace list in home page
+ this.registerWorkspaceListToHome(core, contentManagement);
+
return {};
}
+ private registerWorkspaceListToHome(
+ core: CoreStart,
+ contentManagement: ContentManagementPluginStart
+ ) {
+ if (contentManagement) {
+ contentManagement.registerContentProvider({
+ id: HOME_PAGE_ID,
+ 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,
};