Skip to content

Commit

Permalink
[Workspace] Register four get started cards in home page (#7333)
Browse files Browse the repository at this point in the history
* support get start card in home page

Signed-off-by: yubonluo <[email protected]>

* Changeset file for PR #7333 created/updated

* fix unit test errors

Signed-off-by: yubonluo <[email protected]>

* optimize the code

Signed-off-by: yubonluo <[email protected]>

---------

Signed-off-by: yubonluo <[email protected]>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
  • Loading branch information
yubonluo and opensearch-changeset-bot[bot] committed Jul 21, 2024
1 parent 54cbaaa commit 08c2a00
Show file tree
Hide file tree
Showing 17 changed files with 522 additions and 47 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7333.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [Workspace] Register four get started cards in home page ([#7333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7333))
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { CardList } from './card_list';

export const CARD_CONTAINER = 'CARD_CONTAINER';

export type CardContainerInput = ContainerInput<{ description: string; onClick?: () => void }>;
export type CardContainerInput = ContainerInput<{
description: string;
onClick?: () => void;
getIcon?: () => React.ReactElement;
getFooter?: () => React.ReactElement;
}>;

export class CardContainer extends Container<{}, CardContainerInput> {
public readonly type = CARD_CONTAINER;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { CardEmbeddable } from './card_embeddable';

test('CardEmbeddable should render a card with the title', () => {
const embeddable = new CardEmbeddable({ id: 'card-id', title: 'card title', description: '' });
const embeddable = new CardEmbeddable({
id: 'card-id',
title: 'card title',
description: '',
getIcon: () => <>icon</>,
getFooter: () => <>footer</>,
});

const node = document.createElement('div');
embeddable.render(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { EuiCard } from '@elastic/eui';
import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public';

export const CARD_EMBEDDABLE = 'card_embeddable';
export type CardEmbeddableInput = EmbeddableInput & { description: string; onClick?: () => void };
export type CardEmbeddableInput = EmbeddableInput & {
description: string;
onClick?: () => void;
getIcon: () => React.ReactElement;
getFooter: () => React.ReactElement;
};

export class CardEmbeddable extends Embeddable<CardEmbeddableInput> {
public readonly type = CARD_EMBEDDABLE;
Expand All @@ -27,10 +32,13 @@ export class CardEmbeddable extends Embeddable<CardEmbeddableInput> {
this.node = node;
ReactDOM.render(
<EuiCard
textAlign="left"
title={this.input.title ?? ''}
description={this.input.description}
display="plain"
onClick={this.input.onClick}
icon={this.input?.getIcon()}
footer={this.input?.getFooter()}
/>,
node
);
Expand Down
25 changes: 16 additions & 9 deletions src/plugins/content_management/public/components/page_render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React from 'react';
import { useObservable } from 'react-use';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';

import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Page } from '../services';
import { SectionRender } from './section_render';
import { EmbeddableStart } from '../../../embeddable/public';
Expand All @@ -21,16 +22,22 @@ export const PageRender = ({ page, embeddable, savedObjectsClient }: Props) => {
const sections = useObservable(page.getSections$()) || [];

return (
<div className="contentManagement-page" style={{ margin: '10px 20px' }}>
<EuiFlexGroup
direction="column"
className="contentManagement-page"
style={{ margin: '10px 20px' }}
>
{sections.map((section) => (
<SectionRender
key={section.id}
embeddable={embeddable}
section={section}
savedObjectsClient={savedObjectsClient}
contents$={page.getContents$(section.id)}
/>
<EuiFlexItem>
<SectionRender
key={section.id}
embeddable={embeddable}
section={section}
savedObjectsClient={savedObjectsClient}
contents$={page.getContents$(section.id)}
/>
</EuiFlexItem>
))}
</div>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const createCardInput = (
title: content.title,
description: content.description,
onClick: content.onClick,
getIcon: content?.getIcon,
getFooter: content?.getFooter,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs';
import { EuiTitle } from '@elastic/eui';
import { EuiButtonIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { SavedObjectsClientContract } from 'opensearch-dashboards/public';

import { Content, Section } from '../services';
import { EmbeddableInput, EmbeddableRenderer, EmbeddableStart } from '../../../embeddable/public';
import { DashboardContainerInput } from '../../../dashboard/public';
Expand Down Expand Up @@ -49,6 +48,10 @@ 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 @@ -58,12 +61,24 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => {

if (section.kind === 'card' && factory && input) {
return (
<div style={{ padding: '0 8px' }}>
<EuiPanel>
<EuiTitle size="s">
<h2>{section.title}</h2>
<h2>
<EuiButtonIcon
iconType={isCardVisible ? 'arrowDown' : 'arrowUp'}
onClick={toggleCardVisibility}
color="text"
aria-label={isCardVisible ? 'Show panel' : 'Hide panel'}
/>
{section.title}
</h2>
</EuiTitle>
<EmbeddableRenderer factory={factory} input={input} />
</div>
{isCardVisible && (
<>
<EuiSpacer size="m" /> <EmbeddableRenderer factory={factory} input={input} />
</>
)}
</EuiPanel>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type Content =
title: string;
description: string;
onClick?: () => void;
getIcon?: () => React.ReactElement;
getFooter?: () => React.ReactElement;
};

export type SavedObjectInput =
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/home/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions src/plugins/workspace/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"savedObjects",
"opensearchDashboardsReact"
],
"optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement"],
"requiredBundles": ["opensearchDashboardsReact"]
"optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"],
"requiredBundles": ["opensearchDashboardsReact", "home"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { UseCaseFooter as UseCaseFooterComponent, UseCaseFooterProps } from './use_case_footer';
import { coreMock, httpServiceMock } from '../../../../../core/public/mocks';
import { IntlProvider } from 'react-intl';
import { WorkspaceUseCase } from '../../types';
import { CoreStart } from 'opensearch-dashboards/public';
import { BehaviorSubject } from 'rxjs';
import { WORKSPACE_USE_CASES } from '../../../common/constants';

describe('UseCaseFooter', () => {
// let coreStartMock: CoreStart;
const navigateToApp = jest.fn();
const registeredUseCases$ = new BehaviorSubject([
WORKSPACE_USE_CASES.observability,
WORKSPACE_USE_CASES['security-analytics'],
WORKSPACE_USE_CASES.analytics,
WORKSPACE_USE_CASES.search,
]);

const getMockCore = (isDashboardAdmin: boolean = true) => {
const coreStartMock = coreMock.createStart();
coreStartMock.application.capabilities = {
...coreStartMock.application.capabilities,
dashboards: { isDashboardAdmin },
};
coreStartMock.application = {
...coreStartMock.application,
navigateToApp,
};
jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => {
return `https://test.com/app/${appId}`;
});
return coreStartMock;
};

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});

const UseCaseFooter = (props: UseCaseFooterProps) => {
return (
<IntlProvider locale="en">
<UseCaseFooterComponent {...props} />
</IntlProvider>
);
};
it('renders create workspace button for admin when no workspaces within use case exist', () => {
const { getByTestId } = render(
<UseCaseFooter
useCaseId="analytics"
useCaseTitle="Analytics"
core={getMockCore()}
registeredUseCases$={registeredUseCases$}
/>
);

const button = getByTestId('useCase.footer.createWorkspace.button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
const createWorkspaceButtonInModal = getByTestId('useCase.footer.modal.create.button');
expect(createWorkspaceButtonInModal).toHaveAttribute(
'href',
'https://test.com/app/workspace_create'
);
});

it('renders create workspace button for non-admin when no workspaces within use case exist', () => {
const { getByTestId } = render(
<UseCaseFooter
useCaseId="analytics"
useCaseTitle="Analytics"
core={getMockCore(false)}
registeredUseCases$={registeredUseCases$}
/>
);

const button = getByTestId('useCase.footer.createWorkspace.button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText('Unable to create workspace')).toBeInTheDocument();
expect(screen.queryByTestId('useCase.footer.modal.create.button')).not.toBeInTheDocument();
fireEvent.click(getByTestId('useCase.footer.modal.close.button'));
});

it('renders open workspace button when one workspace exists', () => {
const core = getMockCore();
core.workspaces.workspaceList$.next([
{ id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
]);
const { getByTestId } = render(
<UseCaseFooter
useCaseId="observability"
useCaseTitle="Observability"
core={core}
registeredUseCases$={registeredUseCases$}
/>
);

const button = getByTestId('useCase.footer.openWorkspace.button');
expect(button).toBeInTheDocument();
expect(button).not.toBeDisabled();
expect(button).toHaveAttribute('href', 'https://test.com/w/workspace-1/app/discover');
});

it('renders select workspace popover when multiple workspaces exist', () => {
const core = getMockCore();
core.workspaces.workspaceList$.next([
{ id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] },
{ id: 'workspace-2', name: 'workspace 2', features: ['use-case-observability'] },
]);

const originalLocation = window.location;
Object.defineProperty(window, 'location', {
value: {
assign: jest.fn(),
},
});

render(
<UseCaseFooter
useCaseId="observability"
useCaseTitle="Observability"
core={core}
registeredUseCases$={registeredUseCases$}
/>
);

const button = screen.getByText('Select workspace');
expect(button).toBeInTheDocument();

fireEvent.click(button);
expect(screen.getByText('workspace 1')).toBeInTheDocument();
expect(screen.getByText('workspace 2')).toBeInTheDocument();
expect(screen.getByText('Observability Workspaces')).toBeInTheDocument();

const inputElement = screen.getByPlaceholderText('Search');
expect(inputElement).toBeInTheDocument();
fireEvent.change(inputElement, { target: { value: 'workspace 1' } });
expect(screen.queryByText('workspace 2')).toBeNull();

fireEvent.click(screen.getByText('workspace 1'));
expect(window.location.assign).toHaveBeenCalledWith(
'https://test.com/w/workspace-1/app/discover'
);
Object.defineProperty(window, 'location', {
value: originalLocation,
});
});
});
Loading

0 comments on commit 08c2a00

Please sign in to comment.