Skip to content

Commit

Permalink
[Workspace] Refactor get start card at new home page (#7920)
Browse files Browse the repository at this point in the history
* refactor started card

Signed-off-by: yubonluo <yubonluo@amazon.com>

* refactor new home page get start card

Signed-off-by: yubonluo <yubonluo@amazon.com>

* Changeset file for PR #7920 created/updated

* revert code

Signed-off-by: yubonluo <yubonluo@amazon.com>

* optimize the code

Signed-off-by: yubonluo <yubonluo@amazon.com>

---------

Signed-off-by: yubonluo <yubonluo@amazon.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
(cherry picked from commit d13007a)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 0c7b210 commit 69c9e7f
Show file tree
Hide file tree
Showing 14 changed files with 601 additions and 427 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7920.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
refactor:
- [Workspace] Refactor get start card at new home page ([#7920](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7920))
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import React from 'react';
import ReactDOM from 'react-dom';
import { EuiCard, EuiCardProps } from '@elastic/eui';
import { EuiCard, EuiCardProps, EuiToolTip } from '@elastic/eui';

import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public';

export const CARD_EMBEDDABLE = 'card_embeddable';
export type CardEmbeddableInput = EmbeddableInput & {
description: string;
toolTipContent?: string;
getTitle?: () => React.ReactElement;
onClick?: () => void;
getIcon?: () => React.ReactElement;
getFooter?: () => React.ReactElement;
Expand All @@ -34,8 +36,12 @@ export class CardEmbeddable extends Embeddable<CardEmbeddableInput> {

const cardProps: EuiCardProps = {
...this.input.cardProps,
title: this.input.title ?? '',
description: this.input.description,
title: (this.input?.getTitle?.() || this.input?.title) ?? '',
description: (
<EuiToolTip position="top" content={this.input?.toolTipContent}>
<>{this.input.description}</>
</EuiToolTip>
),
onClick: this.input.onClick,
icon: this.input?.getIcon?.(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { EuiCardProps } from '@elastic/eui';
import { ContainerInput } from '../../../../embeddable/public';

export interface CardExplicitInput {
title: string;
title?: string;
description: string;
toolTipContent?: string;
getTitle?: () => React.ReactElement;
onClick?: () => void;
getIcon?: () => React.ReactElement;
getFooter?: () => React.ReactElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ export const createCardInput = (
type: CARD_EMBEDDABLE,
explicitInput: {
id: content.id,
title: content.title,
title: content?.title,
description: content.description,
toolTipContent: content?.toolTipContent,
getTitle: content?.getTitle,
onClick: content.onClick,
getIcon: content?.getIcon,
getFooter: content?.getFooter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs';
import { EuiButtonIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { EuiButtonIcon, EuiSpacer, EuiTitle } from '@elastic/eui';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';
import { Content, Section } from '../services';
import { EmbeddableInput, EmbeddableRenderer, EmbeddableStart } from '../../../embeddable/public';
Expand Down Expand Up @@ -54,9 +54,6 @@ const DashboardSection = ({ section, embeddable, contents$, savedObjectsClient }

const CardSection = ({ section, embeddable, contents$ }: Props) => {
const [isCardVisible, setIsCardVisible] = useState(true);
const toggleCardVisibility = () => {
setIsCardVisible(!isCardVisible);
};
const contents = useObservable(contents$);
const input = useMemo(() => {
return createCardInput(section, contents ?? []);
Expand All @@ -66,13 +63,13 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => {

if (section.kind === 'card' && factory && input) {
return (
<EuiPanel paddingSize="none" hasBorder={false} hasShadow={false} color="transparent">
<>
{section.title ? (
<EuiTitle size="s">
<h2>
<EuiButtonIcon
iconType={isCardVisible ? 'arrowDown' : 'arrowUp'}
onClick={toggleCardVisibility}
onClick={() => setIsCardVisible(!isCardVisible)}

Check warning on line 72 in src/plugins/content_management/public/components/section_render.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/content_management/public/components/section_render.tsx#L72

Added line #L72 was not covered by tests
color="text"
aria-label={isCardVisible ? 'Show panel' : 'Hide panel'}
/>
Expand All @@ -85,7 +82,7 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => {
<EuiSpacer size="s" /> <EmbeddableRenderer factory={factory} input={input} />
</>
)}
</EuiPanel>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ export type Content =
kind: 'card';
id: string;
order: number;
title: string;
title?: string;
description: string;
toolTipContent?: string;
getTitle?: () => React.ReactElement;
onClick?: () => void;
getIcon?: () => React.ReactElement;
getFooter?: () => React.ReactElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { UseCaseFooter } from './use_case_footer';
export { UseCaseCardTitle } from './use_case_card_title';
export { registerGetStartedCardToNewHome } from './setup_get_start_card';
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { ContentManagementPluginStart } from '../../../../../plugins/content_management/public';
import { coreMock } from '../../../../../core/public/mocks';
import { registerGetStartedCardToNewHome } from './setup_get_start_card';
import { createMockedRegisteredUseCases$ } from '../../mocks';
import { WorkspaceObject } from 'opensearch-dashboards/public';

describe('Setup use get start card at new home page', () => {
const navigateToApp = jest.fn();

const getMockCore = (workspaceList: WorkspaceObject[], isDashboardAdmin: boolean) => {
const coreStartMock = coreMock.createStart();
coreStartMock.application.capabilities = {
...coreStartMock.application.capabilities,
dashboards: { isDashboardAdmin },
};
coreStartMock.workspaces.workspaceList$.next(workspaceList);
coreStartMock.application = {
...coreStartMock.application,
navigateToApp,
};
jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => {
return `https://test.com/app/${appId}`;
});
return coreStartMock;
};
const registerContentProviderMock = jest.fn();
const registeredUseCases$ = createMockedRegisteredUseCases$();
const useCasesMock = [
{
id: 'dataAdministration',
title: 'Data administration',
description: 'Apply policies or security on your data.',
features: [
{
id: 'data_administration_landing',
title: 'Overview',
},
],
systematic: true,
order: 1000,
},
{
id: 'essentials',
title: 'Essentials',
description:
'Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.',
features: [
{
id: 'essentials_overview',
title: 'Overview',
},
{
id: 'discover',
title: 'Discover',
},
],
systematic: false,
order: 7000,
},
];
registeredUseCases$.next(useCasesMock);

const contentManagementStartMock: ContentManagementPluginStart = {
registerContentProvider: registerContentProviderMock,
renderPage: jest.fn(),
updatePageSection: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
});

it('should return a tooltip message when there are no workspaces and the user is not dashboard admin', () => {
const core = getMockCore([], false);
registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$);

const calls = registerContentProviderMock.mock.calls;
expect(calls.length).toBe(1);

const firstCall = calls[0];
expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`
Array [
"osd_homepage/get_started",
]
`);
expect(firstCall[0].getContent()).toMatchInlineSnapshot(`
Object {
"cardProps": Object {
"layout": "horizontal",
},
"description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.",
"getIcon": [Function],
"id": "essentials",
"kind": "card",
"order": 1000,
"title": "Essentials",
"toolTipContent": "Contact your administrator to create a workspace or to be added to an existing one.",
}
`);
});

it('should return a getTitle function when there are no workspaces and the user is dashboard admin', () => {
const core = getMockCore([], true);
registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$);

const calls = registerContentProviderMock.mock.calls;
expect(calls.length).toBe(1);

const firstCall = calls[0];
expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`
Array [
"osd_homepage/get_started",
]
`);
expect(firstCall[0].getContent()).toMatchInlineSnapshot(`
Object {
"cardProps": Object {
"layout": "horizontal",
},
"description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.",
"getIcon": [Function],
"getTitle": [Function],
"id": "essentials",
"kind": "card",
"order": 1000,
}
`);
});

it('should return a getTitle function for multiple workspaces', () => {
const workspaces = [
{ id: 'workspace-1', name: 'workspace 1', features: ['use-case-essentials'] },
{ id: 'workspace-2', name: 'workspace 2', features: ['use-case-essentials'] },
];
const core = getMockCore(workspaces, true);
registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$);

const calls = registerContentProviderMock.mock.calls;
expect(calls.length).toBe(1);

const firstCall = calls[0];
expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`
Array [
"osd_homepage/get_started",
]
`);
expect(firstCall[0].getContent()).toMatchInlineSnapshot(`
Object {
"cardProps": Object {
"layout": "horizontal",
},
"description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.",
"getIcon": [Function],
"getTitle": [Function],
"id": "essentials",
"kind": "card",
"order": 1000,
}
`);
});

it('should return a clickable card when there is one workspace', () => {
const workspaces = [
{ id: 'workspace-1', name: 'workspace 1', features: ['use-case-essentials'] },
];
const core = getMockCore(workspaces, true);
registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$);

const calls = registerContentProviderMock.mock.calls;
expect(calls.length).toBe(1);

const firstCall = calls[0];
expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`
Array [
"osd_homepage/get_started",
]
`);
expect(firstCall[0].getContent()).toMatchInlineSnapshot(`
Object {
"cardProps": Object {
"layout": "horizontal",
},
"description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.",
"getIcon": [Function],
"id": "essentials",
"kind": "card",
"onClick": [Function],
"order": 1000,
"title": "Essentials",
}
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { CoreStart } from 'opensearch-dashboards/public';
import { EuiIcon } from '@elastic/eui';
import { BehaviorSubject } from 'rxjs';
import { i18n } from '@osd/i18n';
import {
ContentManagementPluginStart,
HOME_CONTENT_AREAS,
} from '../../../../content_management/public';
import { WorkspaceUseCase } from '../../types';
import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils';
import { UseCaseCardTitle } from './use_case_card_title';

const createContentCard = (useCase: WorkspaceUseCase, core: CoreStart) => {
const { workspaces, application, http } = core;
const workspaceList = workspaces.workspaceList$.getValue();
const isDashboardAdmin = application.capabilities?.dashboards?.isDashboardAdmin;
const filterWorkspaces = workspaceList.filter(
(workspace) => getFirstUseCaseOfFeatureConfigs(workspace?.features || []) === useCase.id
);
if (filterWorkspaces.length === 0 && !isDashboardAdmin) {
return {
title: useCase.title,
toolTipContent: i18n.translate('workspace.getStartCard.noWorkspace.toolTip', {
defaultMessage:
'Contact your administrator to create a workspace or to be added to an existing one.',
}),
};
} else if (filterWorkspaces.length === 1) {
const useCaseUrl = getUseCaseUrl(useCase, filterWorkspaces[0], application, http);
return {
onClick: () => {
application.navigateToUrl(useCaseUrl);

Check warning on line 37 in src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx#L37

Added line #L37 was not covered by tests
},
title: useCase.title,
};
}
return {
getTitle: () =>
React.createElement(UseCaseCardTitle, {

Check warning on line 44 in src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx#L44

Added line #L44 was not covered by tests
filterWorkspaces,
useCase,
core,
}),
};
};

export const registerGetStartedCardToNewHome = (
core: CoreStart,
contentManagement: ContentManagementPluginStart,
registeredUseCases$: BehaviorSubject<WorkspaceUseCase[]>
) => {
const availableUseCases = registeredUseCases$.getValue().filter((item) => !item.systematic);
availableUseCases.forEach((useCase, index) => {
const content = createContentCard(useCase, core);
contentManagement.registerContentProvider({
id: `home_get_start_${useCase.id}`,
getTargetArea: () => [HOME_CONTENT_AREAS.GET_STARTED],
getContent: () => ({
id: useCase.id,
kind: 'card',
order: (index + 1) * 1000,
description: useCase.description,
...content,
getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }),

Check warning on line 69 in src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx#L69

Added line #L69 was not covered by tests
cardProps: {
layout: 'horizontal',
},
}),
});
});
};
Loading

0 comments on commit 69c9e7f

Please sign in to comment.