From 2e400e4850c084b43c575615aef3bc40fbdedd11 Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Thu, 18 Jul 2024 11:13:15 +0800
Subject: [PATCH 1/8] 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,
};
From 28971834d1c0681dfa2e298c3861b846a729e5c4 Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Sun, 21 Jul 2024 09:09:42 +0800
Subject: [PATCH 2/8] fix merge conflicts
Signed-off-by: Hailong Cui
---
src/core/public/index.ts | 2 --
src/plugins/workspace/common/constants.ts | 2 --
src/plugins/workspace/opensearch_dashboards.json | 2 +-
src/plugins/workspace/public/plugin.ts | 1 -
4 files changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 002e1df37846..8c387361a9ca 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -76,7 +76,6 @@ import {
PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
- PersistedLog,
} from './chrome';
import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors';
import { HttpSetup, HttpStart } from './http';
@@ -380,7 +379,6 @@ export {
PersistedLog,
NavGroupItemInMap,
fulfillRegistrationLinksToChromeNavLinks,
- PersistedLog,
};
export { __osdBootstrap__ } from './osd_bootstrap';
diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts
index 6cb0a00ed72a..9fef78152d8d 100644
--- a/src/plugins/workspace/common/constants.ts
+++ b/src/plugins/workspace/common/constants.ts
@@ -185,5 +185,3 @@ 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 5838d070264f..79dff7504bc5 100644
--- a/src/plugins/workspace/opensearch_dashboards.json
+++ b/src/plugins/workspace/opensearch_dashboards.json
@@ -8,5 +8,5 @@
"opensearchDashboardsReact"
],
"optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"],
- "requiredBundles": ["opensearchDashboardsReact","home"]
+ "requiredBundles": ["opensearchDashboardsReact"]
}
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 666f8cdde674..9fa09994d28c 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -47,7 +47,6 @@ 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';
From f8f1213453a956f5ec65ef3882e79071df81bb70 Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Sun, 21 Jul 2024 09:29:42 +0800
Subject: [PATCH 3/8] add home as requiredBundles
Signed-off-by: Hailong Cui
---
src/plugins/workspace/opensearch_dashboards.json | 2 +-
src/plugins/workspace/public/plugin.ts | 3 ---
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json
index 79dff7504bc5..99a66fb1743a 100644
--- a/src/plugins/workspace/opensearch_dashboards.json
+++ b/src/plugins/workspace/opensearch_dashboards.json
@@ -8,5 +8,5 @@
"opensearchDashboardsReact"
],
"optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"],
- "requiredBundles": ["opensearchDashboardsReact"]
+ "requiredBundles": ["opensearchDashboardsReact", "home"]
}
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 9fa09994d28c..82109854bc03 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -275,9 +275,6 @@ export class WorkspacePlugin
// Add workspace id to recent workspaces.
recentWorkspaceManager.addRecentWorkspace(workspaceId);
})();
-
- // Add workspace id to recent workspaces.
- recentWorkspaceManager.addRecentWorkspace(workspaceId);
}
}
From ad65e00f2d8bd8f13646fac46502ff7ddcbf8b4d Mon Sep 17 00:00:00 2001
From: "opensearch-changeset-bot[bot]"
<154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
Date: Sun, 21 Jul 2024 01:33:48 +0000
Subject: [PATCH 4/8] Changeset file for PR #7247 created/updated
---
changelogs/fragments/7247.yml | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 changelogs/fragments/7247.yml
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
From 9a5846da2a696ed35e9488770fb75c426f3acc0e Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Sun, 21 Jul 2024 10:42:13 +0800
Subject: [PATCH 5/8] fix failed UT
Signed-off-by: Hailong Cui
---
src/plugins/workspace/public/plugin.test.ts | 30 +++++++++++++++------
1 file changed, 22 insertions(+), 8 deletions(-)
diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts
index b2ed55c08de6..678985a7e26e 100644
--- a/src/plugins/workspace/public/plugin.test.ts
+++ b/src/plugins/workspace/public/plugin.test.ts
@@ -26,8 +26,15 @@ describe('Workspace plugin', () => {
...coreMock.createSetup(),
chrome: chromeServiceMock.createSetupContract(),
});
+ const registerContentProviderMock = jest.fn();
+ const contentManagementMock = {
+ registerContentProvider: registerContentProviderMock,
+ renderPage: jest.fn(),
+ };
+
beforeEach(() => {
WorkspaceClientMock.mockClear();
+ registerContentProviderMock.mockClear();
Object.values(workspaceClientMock).forEach((item) => item.mockClear());
});
it('#setup', async () => {
@@ -48,7 +55,7 @@ describe('Workspace plugin', () => {
const setupMock = getSetupMock();
const coreStart = coreMock.createStart();
await workspacePlugin.setup(setupMock, {});
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
coreStart.workspaces.currentWorkspaceId$.next('foo');
expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo');
expect(setupMock.application.register).toBeCalledTimes(4);
@@ -182,7 +189,7 @@ describe('Workspace plugin', () => {
const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]);
startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(startMock);
+ workspacePlugin.start(startMock, { contentManagement: contentManagementMock });
expect(startMock.chrome.setBreadcrumbs).toBeCalledWith(
expect.arrayContaining([
expect.objectContaining({
@@ -208,10 +215,17 @@ describe('Workspace plugin', () => {
]);
startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(startMock);
+ workspacePlugin.start(startMock, { contentManagement: contentManagementMock });
expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled();
});
+ it('#start should register workspace list card into new home page', async () => {
+ const startMock = coreMock.createStart();
+ const workspacePlugin = new WorkspacePlugin();
+ workspacePlugin.start(startMock, { contentManagement: contentManagementMock });
+ expect(registerContentProviderMock).toBeCalledTimes(1);
+ });
+
it('#start should call navGroupUpdater$.next after currentWorkspace set', async () => {
const workspacePlugin = new WorkspacePlugin();
const setupMock = getSetupMock();
@@ -225,7 +239,7 @@ describe('Workspace plugin', () => {
jest.spyOn(navGroupUpdater$, 'next');
expect(navGroupUpdater$.next).not.toHaveBeenCalled();
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
waitFor(() => {
expect(navGroupUpdater$.next).toHaveBeenCalled();
@@ -236,7 +250,7 @@ describe('Workspace plugin', () => {
const coreStart = coreMock.createStart();
coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true);
const workspacePlugin = new WorkspacePlugin();
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1);
});
@@ -265,7 +279,7 @@ describe('Workspace plugin', () => {
const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0];
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
const appUpdater = await appUpdater$.pipe(first()).toPromise();
@@ -286,7 +300,7 @@ describe('Workspace plugin', () => {
const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0];
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise();
@@ -337,7 +351,7 @@ describe('Workspace plugin', () => {
const appUpdaterChangeMock = jest.fn();
appUpdater$.subscribe(appUpdaterChangeMock);
- workspacePlugin.start(coreStart);
+ workspacePlugin.start(coreStart, { contentManagement: contentManagementMock });
// Wait for filterNav been executed
await new Promise(setImmediate);
From 0e6fddc167b2f59ec0374fc2ff5ca26e7fcc3bec Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Sun, 21 Jul 2024 11:53:44 +0800
Subject: [PATCH 6/8] address review comments
Signed-off-by: Hailong Cui
---
.../service_card/workspace_list_card.tsx | 64 ++++++-------------
1 file changed, 21 insertions(+), 43 deletions(-)
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
index caa918b9f4a1..451fd33e90ad 100644
--- a/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
+++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
@@ -23,7 +23,7 @@ import {
import { i18n } from '@osd/i18n';
import { Subscription } from 'rxjs';
import moment from 'moment';
-import _ from 'lodash';
+import { orderBy } from 'lodash';
import { CoreStart, WorkspaceObject } from '../../../../../core/public';
import { navigateToWorkspaceDetail } from '../utils/workspace';
@@ -50,7 +50,6 @@ export interface WorkspaceListItem {
export interface WorkspaceListCardState {
availiableWorkspaces: WorkspaceObject[];
filter: string;
- workspaceList: WorkspaceListItem[];
recentWorkspaces: WorkspaceEntry[];
}
@@ -60,64 +59,43 @@ export class WorkspaceListCard extends Component {
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();
- }
+ componentWillUnmount() {
+ this.workspaceSub?.unsubscribe();
}
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,
- })),
- });
+ return 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,
- })),
- });
+ return 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();
+ return [];
}
private handleSwitchWorkspace = (id: string) => {
@@ -128,7 +106,7 @@ export class WorkspaceListCard extends Component
Date: Sun, 21 Jul 2024 13:59:05 +0800
Subject: [PATCH 7/8] update to funtional component
Signed-off-by: Hailong Cui
---
.../workspace_list_card.test.tsx.snap | 2 +-
.../service_card/workspace_list_card.tsx | 279 ++++++++----------
2 files changed, 128 insertions(+), 153 deletions(-)
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
index ca48a73e7a40..35970676eb7e 100644
--- 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
@@ -24,7 +24,7 @@ exports[`workspace list card render normally should show workspace list card cor
class="euiToolTipAnchor"
>
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
index 451fd33e90ad..12b14325ce11 100644
--- a/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
+++ b/src/plugins/workspace/public/components/service_card/workspace_list_card.tsx
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { Component } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import {
EuiPanel,
EuiLink,
@@ -21,16 +21,15 @@ import {
EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
-import { Subscription } from 'rxjs';
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 { WorkspaceEntry, recentWorkspaceManager } from '../../recent_workspace_manager';
+import { recentWorkspaceManager } from '../../recent_workspace_manager';
-const WORKSPACE_LIST_CARD_DESCRIPTIOIN = i18n.translate('workspace.list.card.descriptionh', {
+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.',
});
@@ -41,53 +40,32 @@ export interface WorkspaceListCardProps {
core: CoreStart;
}
-export interface WorkspaceListItem {
- id: string;
- name: string;
- time?: string | number;
-}
-
-export interface WorkspaceListCardState {
- availiableWorkspaces: WorkspaceObject[];
- filter: string;
- recentWorkspaces: WorkspaceEntry[];
-}
+export const WorkspaceListCard = (props: WorkspaceListCardProps) => {
+ const [availableWorkspaces, setAvailableWorkspaces] = useState([]);
+ const [filter, setFilter] = useState('viewed');
-export class WorkspaceListCard extends Component {
- private workspaceSub?: Subscription;
- constructor(props: WorkspaceListCardProps) {
- super(props);
- this.state = {
- availiableWorkspaces: [],
- recentWorkspaces: recentWorkspaceManager.getRecentWorkspaces() || [],
- filter: 'viewed',
- };
- }
-
- componentDidMount() {
- this.workspaceSub = this.props.core.workspaces.workspaceList$.subscribe((list) => {
- this.setState({
- availiableWorkspaces: list || [],
- });
+ useEffect(() => {
+ const workspaceSub = props.core.workspaces.workspaceList$.subscribe((list) => {
+ setAvailableWorkspaces(list || []);
});
- }
-
- componentWillUnmount() {
- this.workspaceSub?.unsubscribe();
- }
+ return () => {
+ workspaceSub.unsubscribe();
+ };
+ }, [props.core]);
- private loadWorkspaceListItems() {
- if (this.state.filter === 'viewed') {
- return orderBy(this.state.recentWorkspaces, ['timestamp'], ['desc'])
- .filter((ws) => this.state.availiableWorkspaces.some((a) => a.id === ws.id))
+ 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: this.state.availiableWorkspaces.find((ws) => ws.id === item.id)?.name!,
+ name: availableWorkspaces.find((ws) => ws.id === item.id)?.name!,
time: item.timestamp,
}));
- } else if (this.state.filter === 'updated') {
- return orderBy(this.state.availiableWorkspaces, ['lastUpdatedTime'], ['desc'])
+ } else if (filter === 'updated') {
+ return orderBy(availableWorkspaces, ['lastUpdatedTime'], ['desc'])
.slice(0, MAX_ITEM_IN_LIST)
.map((ws) => ({
id: ws.id,
@@ -96,122 +74,119 @@ export class WorkspaceListCard extends Component {
- const { application, http } = this.props.core;
+ const handleSwitchWorkspace = (id: string) => {
+ const { application, http } = props.core;
if (application && http) {
navigateToWorkspaceDetail({ application, http }, id);
}
};
- render() {
- const workspaceList = this.loadWorkspaceListItems();
- const { application } = this.props.core;
-
- const isDashboardAdmin = application.capabilities.dashboards?.isDashboardAdmin;
-
- return (
-
-
-
-
- Workspaces
-
-
-
-
-
+ 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);
+ }}
+ />
-
- {
- 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 && (
-
-
-
+
+
+
+ {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: (
+ {
- application.navigateToApp(WORKSPACE_CREATE_APP_ID);
+ handleSwitchWorkspace(workspace.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
-
-
- );
- }
-}
+ >
+ {workspace.name}
+
+ ),
+ description: (
+
+ {moment(workspace.time).fromNow()}
+
+ ),
+ }))}
+ />
+ )}
+
+
+ {
+ application.navigateToApp(WORKSPACE_LIST_APP_ID);
+ }}
+ >
+ View all
+
+
+ );
+};
From 3e5263a3d1783a316a57ad6004fc11972f5bd390 Mon Sep 17 00:00:00 2001
From: Hailong Cui
Date: Sun, 21 Jul 2024 22:38:50 +0800
Subject: [PATCH 8/8] udpate content provider id
Signed-off-by: Hailong Cui
---
src/plugins/workspace/public/plugin.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts
index 82109854bc03..5c4f851716f8 100644
--- a/src/plugins/workspace/public/plugin.ts
+++ b/src/plugins/workspace/public/plugin.ts
@@ -48,7 +48,7 @@ import { recentWorkspaceManager } from './recent_workspace_manager';
import { toMountPoint } from '../../opensearch_dashboards_react/public';
import { UseCaseService } from './services/use_case_service';
import { WorkspaceListCard } from './components/service_card';
-import { HOME_CONTENT_AREAS, HOME_PAGE_ID } from '../../home/public';
+import { HOME_CONTENT_AREAS } from '../../home/public';
type WorkspaceAppType = (
params: AppMountParameters,
@@ -427,7 +427,7 @@ export class WorkspacePlugin
) {
if (contentManagement) {
contentManagement.registerContentProvider({
- id: HOME_PAGE_ID,
+ id: 'workspace_list_card_home',
getContent: () => ({
id: 'workspace_list',
kind: 'custom',