Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workspace] Refactor get start card at new home page #7920

Merged
Merged
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;
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
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 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 @@

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 @@
<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();
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
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
Loading