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

+ 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..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',