[Shared UX] Migrate PageTemplate > NoDataPage >ElasticAgent Card (#127505)

* [Shared UX] Migrate PageTemplate > NoDataPage > NoDataCard >ElasticAgentCard to Shared UX

* Add more unit tests

* Add more unit tests

* Fix typescript & unit test

* Fix snapshot

* Add optional property to no_data_card

* update test

* Updating snapshot

* Integrate RedirectAppLinks

* Updating failing snapshots

* Add TODO

* Removed `renderFooter` prop in favor of hiding if `isDisabled`

* Added `max-width` style to NoDataCard

* Nit: Change name of illustration from `logo` to `illustration`

* Undo generic `.svg` type change

Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
Maja Grubic 2022-03-15 23:21:00 +01:00 committed by GitHub
parent c9058b850c
commit dd3af76aa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1251 additions and 18 deletions

View file

@ -2,6 +2,14 @@
exports[`<ExitFullScreenButton /> is rendered 1`] = `
<ServicesProvider
application={
Object {
"currentAppId$": Observable {
"_isScalar": false,
},
"navigateToUrl": [Function],
}
}
docLinks={
Object {
"dataViewsDocsLink": "dummy link",
@ -12,8 +20,14 @@ exports[`<ExitFullScreenButton /> is rendered 1`] = `
"openDataViewEditor": [MockFunction],
}
}
http={
Object {
"addBasePath": [MockFunction],
}
}
permissions={
Object {
"canAccessFleet": true,
"canCreateNewDataView": true,
}
}

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { NoDataCard } from './no_data_page/no_data_card';
export { NoDataCard, ElasticAgentCard } from './no_data_page/no_data_card';

View file

@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElasticAgentCardComponent props button 1`] = `
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
}
}
navigateToUrl={[MockFunction]}
>
<NoDataCard
button="Button"
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
image="test-file-stub"
title="Add Elastic Agent"
/>
</RedirectAppLinks>
`;
exports[`ElasticAgentCardComponent props href 1`] = `
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
}
}
navigateToUrl={[MockFunction]}
>
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
href="some path"
image="test-file-stub"
title="Add Elastic Agent"
/>
</RedirectAppLinks>
`;
exports[`ElasticAgentCardComponent props recommended 1`] = `
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
}
}
navigateToUrl={[MockFunction]}
>
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
image="test-file-stub"
recommended={true}
title="Add Elastic Agent"
/>
</RedirectAppLinks>
`;
exports[`ElasticAgentCardComponent renders 1`] = `
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
}
}
navigateToUrl={[MockFunction]}
>
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
image="test-file-stub"
title="Add Elastic Agent"
/>
</RedirectAppLinks>
`;
exports[`ElasticAgentCardComponent renders with canAccessFleet false 1`] = `
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
"source": Subject {
"_isScalar": false,
"closed": false,
"hasError": false,
"isStopped": false,
"observers": Array [],
"thrownError": null,
},
}
}
navigateToUrl={[MockFunction]}
>
<NoDataCard
description={
<EuiTextColor
color="default"
>
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
</EuiTextColor>
}
image="test-file-stub"
isDisabled={true}
title={
<EuiTextColor
color="default"
>
Contact your administrator
</EuiTextColor>
}
/>
</RedirectAppLinks>
`;

View file

@ -0,0 +1,320 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElasticAgentCard renders 1`] = `
.emotion-0 {
max-width: 400px;
}
<ServicesProvider
application={
Object {
"currentAppId$": Observable {
"_isScalar": false,
},
"navigateToUrl": [Function],
}
}
docLinks={
Object {
"dataViewsDocsLink": "dummy link",
}
}
editors={
Object {
"openDataViewEditor": [MockFunction],
}
}
http={
Object {
"addBasePath": [MockFunction],
}
}
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
permissions={
Object {
"canAccessFleet": true,
"canCreateNewDataView": true,
}
}
platform={
Object {
"setIsFullscreen": [MockFunction],
}
}
>
<ElasticAgentCard>
<ElasticAgentCardComponent
canAccessFleet={true}
currentAppId$={
Observable {
"_isScalar": false,
}
}
href="/app/integrations/browse"
navigateToUrl={[Function]}
>
<RedirectAppLinks
currentAppId$={
Observable {
"_isScalar": false,
}
}
navigateToUrl={[Function]}
>
<div>
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
href="/app/integrations/browse"
image="test-file-stub"
title="Add Elastic Agent"
>
<EuiCard
betaBadgeProps={
Object {
"label": undefined,
}
}
css={
Object {
"maxWidth": 400,
}
}
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
footer={
<EuiButton
fill={true}
>
Add Elastic Agent
</EuiButton>
}
href="/app/integrations/browse"
image="test-file-stub"
paddingSize="l"
title="Add Elastic Agent"
>
<EuiPanel
css="unknown styles"
element="div"
hasShadow={true}
onClick={[Function]}
paddingSize="l"
>
<EuiPanel
className="euiCard euiCard--centerAligned euiCard--isClickable emotion-0"
element="div"
hasShadow={true}
onClick={[Function]}
paddingSize="l"
>
<div
className="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable emotion-0"
onClick={[Function]}
>
<div
className="euiCard__top"
>
<div
className="euiCard__image"
>
<img
alt=""
src="test-file-stub"
/>
</div>
</div>
<div
className="euiCard__content"
>
<EuiTitle
className="euiCard__title"
id="generated-idTitle"
size="s"
>
<span
className="euiTitle euiTitle--small euiCard__title"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
className="euiCard__titleAnchor"
href="/app/integrations/browse"
rel="noreferrer"
>
Add Elastic Agent
</a>
</span>
</EuiTitle>
<EuiText
className="euiCard__description"
id="generated-idDescription"
size="s"
>
<div
className="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
Use Elastic Agent for a simple, unified way to collect data from your machines.
</p>
</div>
</EuiText>
</div>
<div
className="euiCard__footer"
>
<EuiButton
fill={true}
>
<EuiButtonDisplay
baseClassName="euiButton"
disabled={false}
element="button"
fill={true}
isDisabled={false}
type="button"
>
<button
className="euiButton euiButton--primary euiButton--fill"
disabled={false}
style={
Object {
"minWidth": undefined,
}
}
type="button"
>
<EuiButtonContent
className="euiButton__content"
iconSide="left"
textProps={
Object {
"className": "euiButton__text",
}
}
>
<span
className="euiButtonContent euiButton__content"
>
<span
className="euiButton__text"
>
Add Elastic Agent
</span>
</span>
</EuiButtonContent>
</button>
</EuiButtonDisplay>
</EuiButton>
</div>
</div>
</EuiPanel>
</EuiPanel>
</EuiCard>
</NoDataCard>
</div>
</RedirectAppLinks>
</ElasticAgentCardComponent>
</ElasticAgentCard>
</ServicesProvider>
`;

View file

@ -1,8 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoDataCard props button 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned"
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned emotion-0"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiTitle--small euiCard__title"
id="generated-idTitle"
>
Card title
</span>
<div
class="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
Description
</p>
</div>
</div>
<div
class="euiCard__footer"
>
<button
class="euiButton euiButton--primary euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Button
</span>
</span>
</button>
</div>
</div>
`;
exports[`NoDataCard props extends EuiCardProps 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned custom_class emotion-0"
>
<div
class="euiCard__content"
@ -44,8 +95,12 @@ exports[`NoDataCard props button 1`] = `
`;
exports[`NoDataCard props href 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable"
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable emotion-0"
>
<div
class="euiCard__content"
@ -93,9 +148,48 @@ exports[`NoDataCard props href 1`] = `
</div>
`;
exports[`NoDataCard props recommended 1`] = `
exports[`NoDataCard props isDisabled 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned euiCard--hasBetaBadge"
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--subdued euiPanel--noShadow euiPanel--noBorder euiCard euiCard--centerAligned euiCard-isDisabled emotion-0"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiTitle--small euiCard__title"
id="generated-idTitle"
>
<button
aria-describedby=" generated-idDescription"
class="euiCard__titleButton"
disabled=""
>
Card title
</button>
</span>
<div
class="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
Description
</p>
</div>
</div>
</div>
`;
exports[`NoDataCard props recommended 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned euiCard--hasBetaBadge emotion-0"
>
<div
class="euiCard__content"
@ -126,12 +220,34 @@ exports[`NoDataCard props recommended 1`] = `
Recommended
</span>
</span>
<div
class="euiCard__footer"
>
<button
class="euiButton euiButton--primary euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Card title
</span>
</span>
</button>
</div>
</div>
`;
exports[`NoDataCard renders 1`] = `
.emotion-0 {
max-width: 400px;
}
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned"
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned emotion-0"
>
<div
class="euiCard__content"
@ -151,5 +267,23 @@ exports[`NoDataCard renders 1`] = `
</p>
</div>
</div>
<div
class="euiCard__footer"
>
<button
class="euiButton euiButton--primary euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Card title
</span>
</span>
</button>
</div>
</div>
`;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
import { NoDataCard } from './no_data_card';
import { Subject } from 'rxjs';
describe('ElasticAgentCardComponent', () => {
const navigateToUrl = jest.fn();
const currentAppId$ = new Subject<string | undefined>().asObservable();
test('renders', () => {
const component = shallow(
<ElasticAgentCardComponent
canAccessFleet={true}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
expect(component).toMatchSnapshot();
});
test('renders with canAccessFleet false', () => {
const component = shallow(
<ElasticAgentCardComponent
canAccessFleet={false}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
expect(component.find(NoDataCard).props().isDisabled).toBe(true);
expect(component).toMatchSnapshot();
});
describe('props', () => {
test('recommended', () => {
const component = shallow(
<ElasticAgentCardComponent
recommended
canAccessFleet={true}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
expect(component.find(NoDataCard).props().recommended).toBe(true);
expect(component).toMatchSnapshot();
});
test('button', () => {
const component = shallow(
<ElasticAgentCardComponent
button="Button"
canAccessFleet={true}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
expect(component.find(NoDataCard).props().button).toBe('Button');
expect(component).toMatchSnapshot();
});
test('href', () => {
const component = shallow(
<ElasticAgentCardComponent
canAccessFleet={true}
href={'some path'}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
expect(component.find(NoDataCard).props().href).toBe('some path');
expect(component).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiTextColor } from '@elastic/eui';
import { Observable } from 'rxjs';
import { ElasticAgentCardProps } from './types';
import { NoDataCard } from './no_data_card';
import ElasticAgentCardIllustration from './assets/elastic_agent_card.svg';
import { RedirectAppLinks } from '../../../redirect_app_links';
export type ElasticAgentCardComponentProps = ElasticAgentCardProps & {
canAccessFleet: boolean;
navigateToUrl: (url: string) => Promise<void>;
currentAppId$: Observable<string | undefined>;
};
const noPermissionTitle = i18n.translate(
'sharedUX.noDataPage.elasticAgentCard.noPermission.title',
{
defaultMessage: `Contact your administrator`,
}
);
const noPermissionDescription = i18n.translate(
'sharedUX.noDataPage.elasticAgentCard.noPermission.description',
{
defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`,
}
);
const elasticAgentCardTitle = i18n.translate('sharedUX.noDataPage.elasticAgentCard.title', {
defaultMessage: 'Add Elastic Agent',
});
const elasticAgentCardDescription = i18n.translate(
'sharedUX.noDataPage.elasticAgentCard.description',
{
defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`,
}
);
/**
* Creates a specific NoDataCard pointing users to Integrations when `canAccessFleet`
*/
export const ElasticAgentCardComponent: FunctionComponent<ElasticAgentCardComponentProps> = ({
canAccessFleet,
title,
navigateToUrl,
currentAppId$,
...cardRest
}) => {
const noAccessCard = (
<NoDataCard
image={ElasticAgentCardIllustration}
title={<EuiTextColor color="default">{noPermissionTitle}</EuiTextColor>}
description={<EuiTextColor color="default">{noPermissionDescription}</EuiTextColor>}
isDisabled
{...cardRest}
/>
);
const card = (
<NoDataCard
image={ElasticAgentCardIllustration}
title={title || elasticAgentCardTitle}
description={elasticAgentCardDescription}
{...cardRest}
/>
);
return (
<RedirectAppLinks navigateToUrl={navigateToUrl} currentAppId$={currentAppId$}>
{canAccessFleet ? card : noAccessCard}
</RedirectAppLinks>
);
};

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import {
ElasticAgentCardComponent,
ElasticAgentCardComponentProps,
} from './elastic_agent_card.component';
import { applicationServiceFactory } from '../../../../services/storybook/application';
export default {
title: 'Elastic Agent Data Card',
description: 'A solution-specific wrapper around NoDataCard, to be used on NoData page',
};
type Params = Pick<ElasticAgentCardComponentProps, 'canAccessFleet'>;
export const PureComponent = (params: Params) => {
const { currentAppId$, navigateToUrl } = applicationServiceFactory();
return (
<ElasticAgentCardComponent
{...params}
currentAppId$={currentAppId$}
navigateToUrl={navigateToUrl}
/>
);
};
PureComponent.argTypes = {
canAccessFleet: {
control: 'boolean',
defaultValue: true,
},
};

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ReactWrapper } from 'enzyme';
import React from 'react';
import { ElasticAgentCard } from './elastic_agent_card';
import { servicesFactory } from '../../../../services/mocks';
import { ServicesProvider, SharedUXServices } from '../../../../services';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
describe('ElasticAgentCard', () => {
let services: SharedUXServices;
let mount: (element: JSX.Element) => ReactWrapper;
beforeEach(() => {
services = servicesFactory();
mount = (element: JSX.Element) =>
mountWithIntl(<ServicesProvider {...services}>{element}</ServicesProvider>);
});
afterEach(() => {
jest.resetAllMocks();
});
test('renders', () => {
const component = mount(<ElasticAgentCard />);
expect(component).toMatchSnapshot();
});
describe('href', () => {
test('returns href if href is given', () => {
const component = mount(<ElasticAgentCard href={'/take/me/somewhere'} />);
expect(component.find(ElasticAgentCardComponent).props().href).toBe('/take/me/somewhere');
});
test('returns prefix + category if href is not given', () => {
const component = mount(<ElasticAgentCard category={'solutions'} />);
expect(component.find(ElasticAgentCardComponent).props().href).toBe(
'/app/integrations/browse/solutions'
);
});
test('returns prefix if nor category nor href are given', () => {
const component = mount(<ElasticAgentCard />);
expect(component.find(ElasticAgentCardComponent).props().href).toBe(
'/app/integrations/browse'
);
});
});
describe('canAccessFleet', () => {
test('passes in the right parameter', () => {
const component = mount(<ElasticAgentCard />);
expect(component.find(ElasticAgentCardComponent).props().canAccessFleet).toBe(true);
});
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { ElasticAgentCardProps } from './types';
import { useApplication, useHttp, usePermissions } from '../../../../services';
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
export const ElasticAgentCard = (props: ElasticAgentCardProps) => {
const { canAccessFleet } = usePermissions();
const { addBasePath } = useHttp();
const { navigateToUrl, currentAppId$ } = useApplication();
const createHref = () => {
const { href, category } = props;
if (href) {
return href;
}
// TODO: get this URL from a locator
const prefix = '/app/integrations/browse';
if (category) {
return addBasePath(`${prefix}/${category}`);
}
return prefix;
};
return (
<ElasticAgentCardComponent
{...props}
href={createHref()}
canAccessFleet={canAccessFleet}
navigateToUrl={navigateToUrl}
currentAppId$={currentAppId$}
/>
);
};

View file

@ -6,3 +6,4 @@
* Side Public License, v 1.
*/
export { NoDataCard } from './no_data_card';
export { ElasticAgentCard } from './elastic_agent_card';

View file

@ -18,11 +18,7 @@ export default {
type Params = Pick<NoDataCardProps, 'recommended' | 'button' | 'description'>;
export const PureComponent = (params: Params) => {
return (
<div style={{ width: '50%' }}>
<NoDataCard title={'Add data'} {...params} />
</div>
);
return <NoDataCard title={'Add data'} {...params} />;
};
PureComponent.argTypes = {

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const NO_DATA_CARD_MAX_WIDTH = 400;
export const NoDataCardStyles = () => {
return {
maxWidth: NO_DATA_CARD_MAX_WIDTH,
};
};

View file

@ -37,5 +37,29 @@ describe('NoDataCard', () => {
);
expect(component).toMatchSnapshot();
});
test('isDisabled', () => {
const component = render(
<NoDataCard
isDisabled={true}
button="Button"
title="Card title"
description="Description"
/>
);
expect(component).toMatchSnapshot();
});
test('extends EuiCardProps', () => {
const component = render(
<NoDataCard
button="Button"
title="Card title"
description="Description"
className="custom_class"
/>
);
expect(component).toMatchSnapshot();
});
});
});

View file

@ -10,13 +10,14 @@ import { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiButton, EuiCard } from '@elastic/eui';
import type { NoDataCardProps } from './types';
import { NoDataCardStyles } from './no_data_card.styles';
const recommendedLabel = i18n.translate('sharedUX.pageTemplate.noDataPage.recommendedLabel', {
defaultMessage: 'Recommended',
});
const defaultDescription = i18n.translate('sharedUX.pageTemplate.noDataCard.description', {
defaultMessage: `Proceed without collecting data`,
defaultMessage: 'Proceed without collecting data',
});
export const NoDataCard: FunctionComponent<NoDataCardProps> = ({
@ -24,12 +25,21 @@ export const NoDataCard: FunctionComponent<NoDataCardProps> = ({
title,
button,
description,
isDisabled,
...cardRest
}) => {
const styles = NoDataCardStyles();
const footer = () => {
if (typeof button !== 'string') {
// Don't render the footer action if disabled
if (isDisabled) {
return;
}
// Render a custom footer action if the button is not a simple string
if (button && typeof button !== 'string') {
return button;
}
// Default footer action is a button with the provided or default string
return <EuiButton fill>{button || title}</EuiButton>;
};
const label = recommended ? recommendedLabel : undefined;
@ -37,11 +47,13 @@ export const NoDataCard: FunctionComponent<NoDataCardProps> = ({
return (
<EuiCard
css={styles}
paddingSize="l"
title={title!}
description={cardDescription}
betaBadgeProps={{ label }}
footer={footer()}
isDisabled={isDisabled}
{...cardRest}
/>
);

View file

@ -15,7 +15,8 @@ export type NoDataCardProps = Partial<Omit<EuiCardProps, 'layout'>> & {
*/
recommended?: boolean;
/**
* Provide just a string for the button's label, or a whole component
* Provide just a string for the button's label, or a whole component;
* The button will be hidden completely if `isDisabled=true`
*/
button?: string | ReactNode;
/**
@ -23,7 +24,15 @@ export type NoDataCardProps = Partial<Omit<EuiCardProps, 'layout'>> & {
*/
onClick?: MouseEventHandler<HTMLElement>;
/**
* Description for the card. If not provided, the default will be used.
* Description for the card;
* If not provided, the default will be used
*/
description?: string;
description?: string | ReactNode;
};
export type ElasticAgentCardProps = NoDataCardProps & {
/**
* Category to auto-select within Fleet
*/
category?: string;
};

View file

@ -44,7 +44,6 @@ export const RedirectAppLinks: FunctionComponent<RedirectAppLinksProps> = ({
}) => {
const currentAppId = useObservable(currentAppId$, undefined);
const containerRef = useRef<HTMLDivElement>(null);
const clickHandler = useMemo(
() =>
containerRef.current && currentAppId

View file

@ -2,6 +2,14 @@
exports[`<SolutionToolbarButton /> is rendered 1`] = `
<ServicesProvider
application={
Object {
"currentAppId$": Observable {
"_isScalar": false,
},
"navigateToUrl": [Function],
}
}
docLinks={
Object {
"dataViewsDocsLink": "dummy link",
@ -12,8 +20,14 @@ exports[`<SolutionToolbarButton /> is rendered 1`] = `
"openDataViewEditor": [MockFunction],
}
}
http={
Object {
"addBasePath": [MockFunction],
}
}
permissions={
Object {
"canAccessFleet": true,
"canCreateNewDataView": true,
}
}

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
export interface SharedUXApplicationService {
navigateToUrl: (url: string) => Promise<void>;
currentAppId$: Observable<string | undefined>;
}

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface SharedUXHttpService {
addBasePath: (url: string) => string;
}

View file

@ -12,6 +12,8 @@ import { servicesFactory } from './stub';
import { SharedUXUserPermissionsService } from './permissions';
import { SharedUXEditorsService } from './editors';
import { SharedUXDocLinksService } from './doc_links';
import { SharedUXHttpService } from './http';
import { SharedUXApplicationService } from './application';
/**
* A collection of services utilized by SharedUX. This serves as a thin
@ -26,6 +28,8 @@ export interface SharedUXServices {
permissions: SharedUXUserPermissionsService;
editors: SharedUXEditorsService;
docLinks: SharedUXDocLinksService;
http: SharedUXHttpService;
application: SharedUXApplicationService;
}
// The React Context used to provide the services to the SharedUX components.
@ -60,3 +64,7 @@ export const usePermissions = () => useServices().permissions;
export const useEditors = () => useServices().editors;
export const useDocLinks = () => useServices().docLinks;
export const useHttp = () => useServices().http;
export const useApplication = () => useServices().application;

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { KibanaPluginServiceFactory } from '../types';
import { SharedUXPluginStartDeps } from '../../types';
import { SharedUXApplicationService } from '../application';
export type ApplicationServiceFactory = KibanaPluginServiceFactory<
SharedUXApplicationService,
SharedUXPluginStartDeps
>;
/**
* A factory function for creating a Kibana-based implementation of `SharedUXEditorsService`.
*/
export const applicationServiceFactory: ApplicationServiceFactory = ({ coreStart }) => ({
navigateToUrl: coreStart.application.navigateToUrl,
currentAppId$: coreStart.application.currentAppId$,
});

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { KibanaPluginServiceFactory } from '../types';
import { SharedUXHttpService } from '../http';
import { SharedUXPluginStartDeps } from '../../types';
export type HttpServiceFactory = KibanaPluginServiceFactory<
SharedUXHttpService,
SharedUXPluginStartDeps
>;
/**
* A factory function for creating a Kibana-based implementation of `SharedUXEditorsService`.
*/
export const httpServiceFactory: HttpServiceFactory = ({ coreStart, startPlugins }) => ({
addBasePath: coreStart.http.basePath.prepend,
});

View file

@ -13,6 +13,8 @@ import { platformServiceFactory } from './platform';
import { userPermissionsServiceFactory } from './permissions';
import { editorsServiceFactory } from './editors';
import { docLinksServiceFactory } from './doc_links';
import { httpServiceFactory } from './http';
import { applicationServiceFactory } from './application';
/**
* A factory function for creating a Kibana-based implementation of `SharedUXServices`.
@ -25,4 +27,6 @@ export const servicesFactory: KibanaPluginServiceFactory<
permissions: userPermissionsServiceFactory(params),
editors: editorsServiceFactory(params),
docLinks: docLinksServiceFactory(params),
http: httpServiceFactory(params),
application: applicationServiceFactory(params),
});

View file

@ -18,6 +18,10 @@ export type UserPermissionsServiceFactory = KibanaPluginServiceFactory<
/**
* A factory function for creating a Kibana-based implementation of `SharedUXPermissionsService`.
*/
export const userPermissionsServiceFactory: UserPermissionsServiceFactory = ({ startPlugins }) => ({
export const userPermissionsServiceFactory: UserPermissionsServiceFactory = ({
coreStart,
startPlugins,
}) => ({
canCreateNewDataView: startPlugins.dataViewEditor.userPermissions.editDataView(),
canAccessFleet: coreStart.application.capabilities.navLinks.integrations,
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '../types';
import { SharedUXApplicationService } from '../application';
import { Observable } from 'rxjs';
export type MockApplicationServiceFactory = PluginServiceFactory<SharedUXApplicationService>;
/**
* A factory function for creating a Jest-based implementation of `SharedUXApplicationService`.
*/
export const applicationServiceFactory: MockApplicationServiceFactory = () => ({
navigateToUrl: () => Promise.resolve(),
currentAppId$: new Observable(),
});

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '../types';
import { SharedUXHttpService } from '../http';
export type MockHttpServiceFactory = PluginServiceFactory<SharedUXHttpService>;
/**
* A factory function for creating a Jest-based implementation of `SharedUXHttpService`.
*/
export const httpServiceFactory: MockHttpServiceFactory = () => ({
addBasePath: jest.fn((path: string) => (path ? path : 'path')),
});

View file

@ -15,6 +15,8 @@ import { PluginServiceFactory } from '../types';
import { platformServiceFactory } from './platform.mock';
import { userPermissionsServiceFactory } from './permissions.mock';
import { editorsServiceFactory } from './editors.mock';
import { httpServiceFactory } from './http.mock';
import { applicationServiceFactory } from './application.mock';
/**
* A factory function for creating a Jest-based implementation of `SharedUXServices`.
@ -24,4 +26,6 @@ export const servicesFactory: PluginServiceFactory<SharedUXServices> = () => ({
permissions: userPermissionsServiceFactory(),
editors: editorsServiceFactory(),
docLinks: docLinksServiceFactory(),
http: httpServiceFactory(),
application: applicationServiceFactory(),
});

View file

@ -17,4 +17,5 @@ export type MockUserPermissionsServiceFactory =
*/
export const userPermissionsServiceFactory: MockUserPermissionsServiceFactory = () => ({
canCreateNewDataView: true,
canAccessFleet: true,
});

View file

@ -8,4 +8,5 @@
export interface SharedUXUserPermissionsService {
canCreateNewDataView: boolean;
canAccessFleet: boolean;
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { BehaviorSubject } from 'rxjs';
import { action } from '@storybook/addon-actions';
import { PluginServiceFactory } from '../types';
import { SharedUXApplicationService } from '../application';
export type ApplicationServiceFactory = PluginServiceFactory<SharedUXApplicationService>;
/**
* A factory function for creating for creating a storybook implementation of `SharedUXApplicationService`.
*/
export const applicationServiceFactory: ApplicationServiceFactory = () => ({
navigateToUrl: () => {
action('NavigateToUrl');
return Promise.resolve();
},
currentAppId$: new BehaviorSubject('123'),
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { action } from '@storybook/addon-actions';
import { PluginServiceFactory } from '../types';
import { SharedUXHttpService } from '../http';
/**
* A factory function for creating a Storybook-based implementation of `SharedUXHttpService`.
*/
export type HttpServiceFactory = PluginServiceFactory<SharedUXHttpService, {}>;
/**
* A factory function for creating a Storybook-based implementation of `SharedUXHttpService`.
*/
export const httpServiceFactory: HttpServiceFactory = () => ({
addBasePath: action('addBasePath') as SharedUXHttpService['addBasePath'],
});

View file

@ -12,6 +12,8 @@ import { platformServiceFactory } from './platform';
import { editorsServiceFactory } from './editors';
import { userPermissionsServiceFactory } from './permissions';
import { docLinksServiceFactory } from './doc_links';
import { httpServiceFactory } from './http';
import { applicationServiceFactory } from './application';
/**
* A factory function for creating a Storybook-based implementation of `SharedUXServices`.
@ -21,4 +23,6 @@ export const servicesFactory: PluginServiceFactory<SharedUXServices, {}> = (para
permissions: userPermissionsServiceFactory(),
editors: editorsServiceFactory(),
docLinks: docLinksServiceFactory(),
http: httpServiceFactory(params),
application: applicationServiceFactory(),
});

View file

@ -17,4 +17,5 @@ export type SharedUXUserPermissionsServiceFactory =
*/
export const userPermissionsServiceFactory: SharedUXUserPermissionsServiceFactory = () => ({
canCreateNewDataView: true,
canAccessFleet: true,
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { PluginServiceFactory } from '../types';
import { SharedUXApplicationService } from '../application';
export type ApplicationServiceFactory = PluginServiceFactory<SharedUXApplicationService>;
/**
* A factory function for creating for creating a simple stubbed implementation of `SharedUXApplicationService`.
*/
export const applicationServiceFactory: ApplicationServiceFactory = () => ({
navigateToUrl: (url) => {
// eslint-disable-next-line no-console
console.log(url);
return Promise.resolve();
},
currentAppId$: new Observable((subscriber) => {
subscriber.next('123');
}),
});

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PluginServiceFactory } from '../types';
import { SharedUXHttpService } from '../http';
/**
* A factory function for creating a simple stubbed implementation of `SharedUXHttpService`.
*/
export type HttpServiceFactory = PluginServiceFactory<SharedUXHttpService>;
/**
* A factory function for creating a simple stubbed implementation of `SharedUXHttpService`.
*/
export const httpServiceFactory: HttpServiceFactory = () => ({
addBasePath: (url: string) => {
return url;
},
});

View file

@ -12,6 +12,8 @@ import { platformServiceFactory } from './platform';
import { userPermissionsServiceFactory } from './permissions';
import { editorsServiceFactory } from './editors';
import { docLinksServiceFactory } from './doc_links';
import { httpServiceFactory } from './http';
import { applicationServiceFactory } from './application';
/**
* A factory function for creating a simple stubbed implemetation of `SharedUXServices`.
@ -21,4 +23,6 @@ export const servicesFactory: PluginServiceFactory<SharedUXServices> = () => ({
permissions: userPermissionsServiceFactory(),
editors: editorsServiceFactory(),
docLinks: docLinksServiceFactory(),
http: httpServiceFactory(),
application: applicationServiceFactory(),
});

View file

@ -19,4 +19,5 @@ export type UserPermissionsServiceFactory = PluginServiceFactory<SharedUXUserPer
*/
export const userPermissionsServiceFactory: UserPermissionsServiceFactory = () => ({
canCreateNewDataView: true,
canAccessFleet: true,
});