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] Register four get started cards in home page #7333

Merged
merged 6 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 CardSection = ({ section, embeddable, contents$ }: Props) => {
const [isCardVisible, setIsCardVisible] = useState(true);
const toggleCardVisibility = () => {
setIsCardVisible(!isCardVisible);

Check warning on line 53 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#L53

Added line #L53 was not covered by tests
};
const contents = useObservable(contents$);
const input = useMemo(() => {
return createCardInput(section, contents ?? []);
Expand All @@ -58,12 +61,24 @@

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
Loading