[Shared UX] Move No Data Cards to packages (#134503)

* [Shared UX] Move No Data Cards to packages

* Consolidate cards

* Fix some Regex mistakes

* Adjust Storybook a bit

* Adjust tests and context arrangement

* Fix bugs; kill bad snapshots; improve tests
This commit is contained in:
Clint Andrew Hall 2022-06-21 11:10:00 -05:00 committed by GitHub
parent 02bc0e97bb
commit e1eb3db916
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1341 additions and 978 deletions

View file

@ -210,6 +210,7 @@
"@kbn/shared-ux-avatar-solution": "link:bazel-bin/packages/shared-ux/avatar/solution",
"@kbn/shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen",
"@kbn/shared-ux-button-toolbar": "link:bazel-bin/packages/shared-ux/button_toolbar",
"@kbn/shared-ux-card-no-data": "link:bazel-bin/packages/shared-ux/card/no_data",
"@kbn/shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components",
"@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app",
"@kbn/shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data",
@ -750,6 +751,7 @@
"@types/kbn__shared-ux-avatar-solution": "link:bazel-bin/packages/shared-ux/avatar/solution/npm_module_types",
"@types/kbn__shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen/npm_module_types",
"@types/kbn__shared-ux-button-toolbar": "link:bazel-bin/packages/shared-ux/button_toolbar/npm_module_types",
"@types/kbn__shared-ux-card-no-data": "link:bazel-bin/packages/shared-ux/card/no_data/npm_module_types",
"@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types",
"@types/kbn__shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/npm_module_types",
"@types/kbn__shared-ux-page-analytics-no-data": "link:bazel-bin/packages/shared-ux/page/analytics_no_data/npm_module_types",

View file

@ -146,6 +146,7 @@ filegroup(
"//packages/shared-ux/avatar/solution:build",
"//packages/shared-ux/button_toolbar:build",
"//packages/shared-ux/button/exit_full_screen:build",
"//packages/shared-ux/card/no_data:build",
"//packages/shared-ux/link/redirect_app:build",
"//packages/shared-ux/page/analytics_no_data:build",
"//packages/shared-ux/page/kibana_no_data:build",
@ -279,6 +280,7 @@ filegroup(
"//packages/shared-ux/avatar/solution:build_types",
"//packages/shared-ux/button_toolbar:build_types",
"//packages/shared-ux/button/exit_full_screen:build_types",
"//packages/shared-ux/card/no_data:build_types",
"//packages/shared-ux/link/redirect_app:build_types",
"//packages/shared-ux/page/analytics_no_data:build_types",
"//packages/shared-ux/page/kibana_no_data:build_types",

View file

@ -45,6 +45,7 @@ RUNTIME_DEPS = [
"//packages/shared-ux/avatar/solution",
"//packages/shared-ux/link/redirect_app",
"//packages/shared-ux/prompt/no_data_views",
"//packages/shared-ux/card/no_data",
"//packages/kbn-shared-ux-services",
"//packages/kbn-shared-ux-storybook",
"//packages/kbn-shared-ux-utility",
@ -74,6 +75,7 @@ TYPES_DEPS = [
"//packages/shared-ux/avatar/solution:npm_module_types",
"//packages/shared-ux/link/redirect_app:npm_module_types",
"//packages/shared-ux/prompt/no_data_views:npm_module_types",
"//packages/shared-ux/card/no_data:npm_module_types",
"//packages/kbn-shared-ux-services:npm_module_types",
"//packages/kbn-shared-ux-storybook:npm_module_types",
"//packages/kbn-shared-ux-utility:npm_module_types",

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { NoDataCard, ElasticAgentCard, NoDataPage, NoDataConfigPage } from './no_data_page';
export { NoDataPage, NoDataConfigPage } from './no_data_page';
export { KibanaPageTemplate } from './page_template';
export type { KibanaPageTemplateProps } from './types';
export type { NoDataPageProps } from './no_data_page';

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoDataPage render 1`] = `
<div
className="kbnNoDataPageContents"
data-test-subj="kbnNoDataPage"
>
<EuiText
textAlign="center"
>
<KibanaSolutionAvatar
iconType="logoKibana"
name="Analytics"
size="xxl"
/>
<EuiSpacer
size="l"
/>
<h1>
Welcome to Elastic Analytics!
</h1>
<EuiTextColor
color="subdued"
>
<p>
<FormattedMessage
defaultMessage="Add your data to get started, or {link} about {solution}."
id="sharedUXComponents.noDataPage.intro"
values={
Object {
"link": <EuiLink
href="test"
target="_blank"
>
<FormattedMessage
defaultMessage="learn more"
id="sharedUXComponents.noDataPage.intro.link"
values={Object {}}
/>
</EuiLink>,
"solution": "Analytics",
}
}
/>
</p>
</EuiTextColor>
</EuiText>
<EuiSpacer
size="xxl"
/>
<ElasticAgentCard
key="empty-page-agent-action"
/>
</div>
`;

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
export { NoDataCard, ElasticAgentCard } from './no_data_card';
export { NoDataPage } from './no_data_page';
export type { NoDataPageProps } from './types';
export { NoDataConfigPage, NoDataConfigPageWithSolutionNavBar } from './no_data_config_page';

View file

@ -1,104 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElasticAgentCardComponent props button 1`] = `
<NoDataCard
button="Button"
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
image={
<EuiImage
alt=""
size="fullWidth"
style={
Object {
"background": "aliceblue",
"height": 240,
"objectFit": "cover",
"width": "max(100%, 360px)",
}
}
url="test-file-stub"
/>
}
title="Add Elastic Agent"
/>
`;
exports[`ElasticAgentCardComponent props href 1`] = `
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
href="some path"
image={
<EuiImage
alt=""
size="fullWidth"
style={
Object {
"background": "aliceblue",
"height": 240,
"objectFit": "cover",
"width": "max(100%, 360px)",
}
}
url="test-file-stub"
/>
}
title="Add Elastic Agent"
/>
`;
exports[`ElasticAgentCardComponent renders 1`] = `
<NoDataCard
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
image={
<EuiImage
alt=""
size="fullWidth"
style={
Object {
"background": "aliceblue",
"height": 240,
"objectFit": "cover",
"width": "max(100%, 360px)",
}
}
url="test-file-stub"
/>
}
title="Add Elastic Agent"
/>
`;
exports[`ElasticAgentCardComponent renders with canAccessFleet false 1`] = `
<NoDataCard
description={
<EuiTextColor
color="default"
>
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
</EuiTextColor>
}
image={
<EuiImage
alt=""
size="fullWidth"
style={
Object {
"background": "aliceblue",
"height": 240,
"objectFit": "cover",
"width": "max(100%, 360px)",
}
}
url="test-file-stub"
/>
}
isDisabled={true}
title={
<EuiTextColor
color="default"
>
Contact your administrator
</EuiTextColor>
}
/>
`;

View file

@ -1,75 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElasticAgentCard renders 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="height: 240px; object-fit: cover; background: aliceblue;"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="/app/integrations/browse"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Add Elastic Agent
</span>
</a>
</span>
<div
class="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>
</div>
<div
class="euiCard__footer"
>
<button
class="euiButton euiButton--primary euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButton__content"
>
<span
class="euiButton__text"
>
Add Elastic Agent
</span>
</span>
</button>
</div>
</div>
</div>
`;

View file

@ -1,231 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoDataCard props button 1`] = `
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned css-1hu4pg0-EuiCard"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</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`] = `
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned custom_class css-1hu4pg0-EuiCard"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</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 href 1`] = `
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="#"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</a>
</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 isDisabled 1`] = `
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--subdued euiPanel--noShadow euiPanel--noBorder euiCard euiCard--centerAligned euiCard-isDisabled css-1hu4pg0-EuiCard"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<button
aria-describedby=" generated-idDescription"
class="euiCard__titleButton"
disabled=""
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</button>
</span>
<div
class="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
Description
</p>
</div>
</div>
</div>
`;
exports[`NoDataCard renders 1`] = `
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned css-1hu4pg0-EuiCard"
>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</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"
>
Card title
</span>
</span>
</button>
</div>
</div>
`;

View file

@ -1,43 +0,0 @@
/*
* 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';
describe('ElasticAgentCardComponent', () => {
test('renders', () => {
const component = shallow(<ElasticAgentCardComponent canAccessFleet={true} />);
expect(component).toMatchSnapshot();
});
test('renders with canAccessFleet false', () => {
const component = shallow(<ElasticAgentCardComponent canAccessFleet={false} />);
expect(component.find(NoDataCard).props().isDisabled).toBe(true);
expect(component).toMatchSnapshot();
});
describe('props', () => {
test('button', () => {
const component = shallow(
<ElasticAgentCardComponent button="Button" canAccessFleet={true} />
);
expect(component.find(NoDataCard).props().button).toBe('Button');
expect(component).toMatchSnapshot();
});
test('href', () => {
const component = shallow(
<ElasticAgentCardComponent canAccessFleet={true} href={'some path'} />
);
expect(component.find(NoDataCard).props().href).toBe('some path');
expect(component).toMatchSnapshot();
});
});
});

View file

@ -1,83 +0,0 @@
/*
* 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 { EuiImage, EuiTextColor } from '@elastic/eui';
import { ElasticAgentCardProps } from './types';
import { NoDataCard } from './no_data_card';
import ElasticAgentCardIllustration from './assets/elastic_agent_card.svg';
export type ElasticAgentCardComponentProps = ElasticAgentCardProps & {
canAccessFleet: boolean;
};
const noPermissionTitle = i18n.translate(
'sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title',
{
defaultMessage: `Contact your administrator`,
}
);
const noPermissionDescription = i18n.translate(
'sharedUXComponents.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(
'sharedUXComponents.noDataPage.elasticAgentCard.title',
{
defaultMessage: 'Add Elastic Agent',
}
);
const elasticAgentCardDescription = i18n.translate(
'sharedUXComponents.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 = elasticAgentCardTitle,
description,
...cardRest
}) => {
const props = canAccessFleet
? {
title,
description: description || elasticAgentCardDescription,
}
: {
title: <EuiTextColor color="default">{noPermissionTitle}</EuiTextColor>,
description: <EuiTextColor color="default">{noPermissionDescription}</EuiTextColor>,
isDisabled: true,
};
const image = (
<EuiImage
size="fullWidth"
style={{
width: 'max(100%, 360px)',
height: 240,
objectFit: 'cover',
background: 'aliceblue',
}}
url={ElasticAgentCardIllustration}
alt=""
/>
);
return <NoDataCard image={image} {...props} {...cardRest} />;
};

View file

@ -1,42 +0,0 @@
/*
* 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 as Component,
ElasticAgentCardComponentProps as ComponentProps,
} from './elastic_agent_card.component';
import { ElasticAgentCard } from './elastic_agent_card';
export default {
title: 'Page Template/No Data/Elastic Agent Data Card',
description: 'A solution-specific wrapper around NoDataCard, to be used on NoData page',
};
type Params = Pick<ComponentProps, 'canAccessFleet'>;
export const PureComponent = (params: Params) => {
return <Component {...params} />;
};
PureComponent.argTypes = {
canAccessFleet: {
control: 'boolean',
defaultValue: true,
},
description: {
control: 'text',
defaultValue: '',
},
};
export const ConnectedComponent = () => {
return <ElasticAgentCard href="#" />;
};

View file

@ -1,78 +0,0 @@
/*
* 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 { mountWithIntl } from '@kbn/test-jest-helpers';
import {
SharedUxServicesProvider,
SharedUxServices,
mockServicesFactory,
} from '@kbn/shared-ux-services';
import { ElasticAgentCard } from './elastic_agent_card';
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
describe('ElasticAgentCard', () => {
let services: SharedUxServices;
let mount: (element: JSX.Element) => ReactWrapper;
beforeEach(() => {
services = mockServicesFactory();
mount = (element: JSX.Element) =>
mountWithIntl(<SharedUxServicesProvider {...services}>{element}</SharedUxServicesProvider>);
});
afterEach(() => {
jest.resetAllMocks();
});
test('renders', () => {
const component = mount(<ElasticAgentCard />);
expect(component.render()).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('description', () => {
test('renders custom description if provided', () => {
const component = mount(
<ElasticAgentCard description="Build seamless search experiences faster." />
);
expect(component.find(ElasticAgentCardComponent).props().description).toBe(
'Build seamless search experiences faster.'
);
});
});
describe('canAccessFleet', () => {
test('passes in the right parameter', () => {
const component = mount(<ElasticAgentCard />);
expect(component.find(ElasticAgentCardComponent).props().canAccessFleet).toBe(true);
});
});
});

View file

@ -1,45 +0,0 @@
/*
* 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, { useMemo } from 'react';
import { useApplication, useHttp, usePermissions } from '@kbn/shared-ux-services';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import useObservable from 'react-use/lib/useObservable';
import { ElasticAgentCardProps } from './types';
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
export const ElasticAgentCard = (props: ElasticAgentCardProps) => {
const { canAccessFleet } = usePermissions();
const { addBasePath } = useHttp();
const { navigateToUrl, currentAppId$ } = useApplication();
const currentAppId = useObservable(currentAppId$);
const { href: srcHref, category, description } = props;
const href = useMemo(() => {
if (srcHref) {
return srcHref;
}
// TODO: get this URL from a locator
const prefix = '/app/integrations/browse';
if (category) {
return addBasePath(`${prefix}/${category}`);
}
return addBasePath(prefix);
}, [addBasePath, srcHref, category]);
return (
<RedirectAppLinks {...{ currentAppId, navigateToUrl }}>
<ElasticAgentCardComponent {...{ ...props, href, canAccessFleet, description }} />
</RedirectAppLinks>
);
};

View file

@ -1,37 +0,0 @@
/*
* 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 { NoDataCard } from './no_data_card';
import type { NoDataCardProps } from './types';
export default {
title: 'Page Template/No Data/No Data Card',
description: 'A wrapper around EuiCard, to be used on NoData page',
};
type Params = Pick<NoDataCardProps, 'button' | 'description'>;
export const PureComponent = (params: Params) => {
return <NoDataCard title={'Add data'} {...params} />;
};
PureComponent.argTypes = {
button: {
control: {
type: 'text',
},
defaultValue: 'Button text',
},
description: {
control: {
type: 'text',
},
defaultValue: 'This is a description',
},
};

View file

@ -1,65 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React, { FunctionComponent } from 'react';
import { EuiButton, EuiCard, EuiScreenReaderOnly } from '@elastic/eui';
import type { NoDataCardProps } from './types';
import { NoDataCardStyles } from './no_data_card.styles';
const defaultDescription = i18n.translate(
'sharedUXComponents.pageTemplate.noDataCard.description',
{
defaultMessage: `Proceed without collecting data`,
}
);
export const NoDataCard: FunctionComponent<NoDataCardProps> = ({
title: titleProp,
button,
description,
isDisabled,
...cardRest
}) => {
const styles = NoDataCardStyles();
const footer = () => {
// 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 || titleProp}</EuiButton>;
};
const cardDescription = description || defaultDescription;
// Fix the need for an a11y title even though the button exists by setting to screen reader only
const title = titleProp ? (
<EuiScreenReaderOnly>
<span>{titleProp}</span>
</EuiScreenReaderOnly>
) : null;
return (
<EuiCard
css={styles}
paddingSize="l"
title={title!}
description={cardDescription}
footer={footer()}
isDisabled={isDisabled}
{...cardRest}
/>
);
};

View file

@ -1,34 +0,0 @@
/*
* 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 { EuiCardProps } from '@elastic/eui';
import { MouseEventHandler, ReactNode } from 'react';
export type NoDataCardProps = Partial<Omit<EuiCardProps, 'layout'>> & {
/**
* 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;
/**
* Remapping `onClick` to any element
*/
onClick?: MouseEventHandler<HTMLElement>;
/**
* Description for the card;
* If not provided, the default will be used
*/
description?: string | ReactNode;
};
export type ElasticAgentCardProps = NoDataCardProps & {
/**
* Category to auto-select within Fleet
*/
category?: string;
};

View file

@ -7,24 +7,27 @@
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { NoDataCard } from '@kbn/shared-ux-card-no-data';
import { SharedUxServicesProvider, mockServicesFactory } from '@kbn/shared-ux-services';
import { NoDataPage } from './no_data_page';
import { shallowWithIntl } from '@kbn/test-jest-helpers';
import { ElasticAgentCard } from './no_data_card';
describe('NoDataPage', () => {
test('render', () => {
const component = shallowWithIntl(
<NoDataPage
solution="Analytics"
action={{
elasticAgent: {},
}}
logo={'logoKibana'}
docsLink="test"
/>
const component = mountWithIntl(
<SharedUxServicesProvider {...mockServicesFactory()}>
<NoDataPage
solution="Analytics"
action={{
elasticAgent: {},
}}
logo={'logoKibana'}
docsLink="test"
/>
</SharedUxServicesProvider>
);
expect(component).toMatchSnapshot();
expect(component.find('h1').html()).toContain('Welcome to Elastic Analytics!');
expect(component.find(ElasticAgentCard).length).toBe(1);
expect(component.find(NoDataCard).length).toBe(1);
});
});

View file

@ -7,6 +7,7 @@
*/
import React, { useMemo, FunctionComponent } from 'react';
import useObservable from 'react-use/lib/useObservable';
import classNames from 'classnames';
import { EuiLink, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui';
@ -14,7 +15,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution';
import { ElasticAgentCard } from './no_data_card';
import { useSharedUxServices } from '@kbn/shared-ux-services';
import { NoDataCard, NoDataCardProvider } from '@kbn/shared-ux-card-no-data';
import { NoDataPageProps } from './types';
export const NoDataPage: FunctionComponent<NoDataPageProps> = ({
@ -25,6 +27,19 @@ export const NoDataPage: FunctionComponent<NoDataPageProps> = ({
pageTitle,
...rest
}) => {
const services = useSharedUxServices();
// TODO: clintandrewhall - including the `NoDataCardProvider` here is a temporary solution
// to consumers using this context to populate the NoDataPage. This will likely be removed soon,
// when NoDataPage is moved to its own package.
const currentAppId = useObservable(services.application.currentAppId$);
const noDataCardServices = {
currentAppId,
addBasePath: services.http.addBasePath,
canAccessFleet: services.permissions.canAccessFleet,
navigateToUrl: services.application.navigateToUrl,
};
const actionKeys = Object.keys(action);
const actionCard = useMemo(() => {
@ -34,7 +49,7 @@ export const NoDataPage: FunctionComponent<NoDataPageProps> = ({
const actionKey = actionKeys[0];
const key =
actionKey === 'elasticAgent' ? 'empty-page-agent-action' : `empty-page-${actionKey}-action`;
return <ElasticAgentCard key={key} {...action[actionKey]} />;
return <NoDataCard key={key} {...action[actionKey]} />;
}, [action, actionKeys]);
const title =
@ -74,7 +89,7 @@ export const NoDataPage: FunctionComponent<NoDataPageProps> = ({
</EuiTextColor>
</EuiText>
<EuiSpacer size="xxl" />
{actionCard}
<NoDataCardProvider {...noDataCardServices}>{actionCard}</NoDataCardProvider>
</div>
);
};

View file

@ -7,9 +7,9 @@
*/
import { CommonProps } from '@elastic/eui';
import { ElasticAgentCardProps } from './no_data_card';
import { NoDataCardProps } from '@kbn/shared-ux-card-no-data';
export type NoDataPageActions = ElasticAgentCardProps;
export type NoDataPageActions = NoDataCardProps;
export interface NoDataPageProps extends CommonProps {
/**

View file

@ -15,4 +15,7 @@ module.exports = {
'../../../kbn-shared-ux*/**/*.stories.+(tsx|mdx)',
'../../../../src/plugins/shared_ux/**/*.stories.+(tsx|mdx)',
],
reactOptions: {
strictMode: true,
},
};

View file

@ -0,0 +1,143 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "no_data"
PKG_REQUIRE_NAME = "@kbn/shared-ux-card-no-data"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.mdx",
"src/**/*.svg",
],
exclude = [
"**/*.test.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//@elastic/eui",
"@npm//@storybook/addon-actions",
"@npm//enzyme",
"@npm//react",
"//packages/kbn-i18n-react",
"//packages/kbn-i18n",
"//packages/shared-ux/link/redirect_app",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@elastic/eui",
"@npm//@storybook/addon-actions",
"@npm//@types/enzyme",
"@npm//@types/jest",
"@npm//@types/node",
"@npm//@types/react",
"//packages/kbn-ambient-ui-types",
"//packages/kbn-i18n-react:npm_module_types",
"//packages/kbn-i18n:npm_module_types",
"//packages/shared-ux/link/redirect_app:npm_module_types",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
jsts_transpiler(
name = "target_web",
srcs = SRCS,
build_pkg_name = package_name(),
web = True,
additional_args = [
"--copy-files",
"--quiet"
],
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
root_dir = "src",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,29 @@
---
id: sharedUX/Components/NoDataCard
slug: /shared-ux/components/no-data-card
title: No Data Card
summary: A card displayed when no data is available is available in Kibana.
tags: ['shared-ux', 'component']
date: 2022-06-15
---
## Description
A wrapper around `EuiCard` tailored for use in Kibana solutions when no data is available.
## Usage
All of the `EuiCard` props are available with the exception of `layout`. A default `description` and `button` are provided, but can be overridden in specific use cases.
The `NoDataCard` connected component uses:
- `navLinks.integrations` from `coreStart.application.capabilities` to determine if the user has access to the Integrations page.
- `addBasePath` from `coreStart` to navigate to the Integrations page.
## API
| Export | Description |
|---|---|
| `NoDataCardProvider` | Provides contextual services to `NoDataCard`. |
| `NoDataCardKibanaProvider` | Maps Kibana dependencies to provide contextual services to `NoDataCard`. |
| `NoDataCard` | Uses a `Provider` to access contextual services to populate props on the `NoDataCardComponent`. |
| `NoDataCardComponent` | The pure component, a pre-configured **EuiCard**. |

View file

@ -5,6 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { NoDataCard } from './no_data_card';
export { ElasticAgentCard } from './elastic_agent_card';
export type { NoDataCardProps, ElasticAgentCardProps } from './types';
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/card/no_data'],
};

View file

@ -0,0 +1,8 @@
{
"name": "@kbn/shared-ux-card-no-data",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,117 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoDataCardComponent props button 1`] = `
<EuiCard
css={
Object {
"marginInline": "auto",
"maxWidth": 400,
}
}
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
footer={
<EuiButton
fill={true}
>
Button
</EuiButton>
}
image={<Image />}
isDisabled={false}
paddingSize="l"
title={
<EuiScreenReaderOnly>
<span>
Add Elastic Agent
</span>
</EuiScreenReaderOnly>
}
/>
`;
exports[`NoDataCardComponent props href 1`] = `
<EuiCard
css={
Object {
"marginInline": "auto",
"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="some path"
image={<Image />}
isDisabled={false}
paddingSize="l"
title={
<EuiScreenReaderOnly>
<span>
Add Elastic Agent
</span>
</EuiScreenReaderOnly>
}
/>
`;
exports[`NoDataCardComponent renders 1`] = `
<EuiCard
css={
Object {
"marginInline": "auto",
"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>
}
image={<Image />}
isDisabled={false}
paddingSize="l"
title={
<EuiScreenReaderOnly>
<span>
Add Elastic Agent
</span>
</EuiScreenReaderOnly>
}
/>
`;
exports[`NoDataCardComponent renders with canAccessFleet false 1`] = `
<EuiCard
css={
Object {
"marginInline": "auto",
"maxWidth": 400,
}
}
description={
<EuiTextColor
color="default"
>
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
</EuiTextColor>
}
image={<Image />}
isDisabled={true}
paddingSize="l"
title={
<EuiTextColor
color="default"
>
Contact your administrator
</EuiTextColor>
}
/>
`;

View file

@ -0,0 +1,356 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoDataCard props button 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="width:max(100%, 360px);height:240px;object-fit:cover;background:aliceblue"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="/app/integrations/browse"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</a>
</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>
</div>
`;
exports[`NoDataCard props extends EuiCardProps 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable custom_class css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="width:max(100%, 360px);height:240px;object-fit:cover;background:aliceblue"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="/app/integrations/browse"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</a>
</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>
</div>
`;
exports[`NoDataCard props href 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="width:max(100%, 360px);height:240px;object-fit:cover;background:aliceblue"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="#"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</a>
</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>
</div>
`;
exports[`NoDataCard props no access to Fleet 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--subdued euiPanel--noShadow euiPanel--noBorder euiCard euiCard--centerAligned euiCard-isDisabled css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="width:max(100%, 360px);height:240px;object-fit:cover;background:aliceblue"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<button
aria-describedby=" generated-idDescription"
class="euiCard__titleButton"
disabled=""
>
<span
class="euiTextColor euiTextColor--default"
>
Contact your administrator
</span>
</button>
</span>
<div
class="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
<span
class="euiTextColor euiTextColor--default"
>
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
</span>
</p>
</div>
</div>
</div>
</div>
`;
exports[`NoDataCard renders 1`] = `
<div>
<div
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
>
<div
class="euiCard__top"
>
<div
class="euiCard__image"
>
<figure
class="euiImage euiImage--fullWidth"
>
<img
alt=""
class="euiImage__img"
src="test-file-stub"
style="width:max(100%, 360px);height:240px;object-fit:cover;background:aliceblue"
/>
</figure>
</div>
</div>
<div
class="euiCard__content"
>
<span
class="euiTitle euiCard__title css-1mr6cet-euiTitle-s"
id="generated-idTitle"
>
<a
aria-describedby="generated-idDescription"
class="euiCard__titleAnchor"
href="/app/integrations/browse"
rel="noreferrer"
>
<span
class="euiScreenReaderOnly"
>
Card title
</span>
</a>
</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"
>
Card title
</span>
</span>
</button>
</div>
</div>
</div>
`;

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.
*/
export { NoDataCard } from './no_data_card';
export type { Props as NoDataCardProps } from './no_data_card';
export { NoDataCardKibanaProvider, NoDataCardProvider } from './services';
export type { NoDataCardKibanaDependencies, NoDataCardServices } from './services';
export {
getMockServices as getNoDataCardMockServices,
getStoryArgTypes as getNoDataCardStoryArgTypes,
getStoryServices as getNoDataCardStoryServices,
} from './mocks';

View file

@ -0,0 +1,88 @@
/*
* 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 {
getRedirectAppLinksMockServices,
getRedirectAppLinksStoryArgTypes,
getRedirectAppLinksStoryServices,
} from '@kbn/shared-ux-link-redirect-app';
import { NoDataCardServices } from './services';
/**
* Parameters drawn from the Storybook arguments collection that customize a component story.
*/
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
/**
* Returns Storybook-compatible service abstractions for the `NoDataCard` Provider.
*/
export const getStoryServices = (params: Params) => {
const services: NoDataCardServices = {
...getRedirectAppLinksStoryServices(),
...params,
addBasePath: (path) => {
action('addBasePath')(path);
return path;
},
};
return services;
};
/**
* Returns the Storybook arguments for `NoDataCard`, for its stories and for
* consuming component stories.
*/
export const getStoryArgTypes = () => ({
...getRedirectAppLinksStoryArgTypes(),
canAccessFleet: {
control: 'boolean',
defaultValue: true,
},
category: {
control: {
type: 'text',
},
defaultValue: '',
},
title: {
control: {
type: 'text',
},
defaultValue: '',
},
description: {
control: {
type: 'text',
},
defaultValue: '',
},
button: {
control: {
type: 'text',
},
defaultValue: '',
},
});
/**
* Returns the Jest-compatible service abstractions for the `NoDataCard` Provider.
*/
export const getMockServices = (params?: Params) => {
const { canAccessFleet } = params || { canAccessFleet: true };
const services: NoDataCardServices = {
...getRedirectAppLinksMockServices(),
canAccessFleet,
addBasePath: (path) => path,
};
return services;
};

View file

@ -0,0 +1,36 @@
/*
* 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 { shallow } from 'enzyme';
import { NoDataCard } from './no_data_card.component';
describe('NoDataCardComponent', () => {
test('renders', () => {
const component = shallow(<NoDataCard canAccessFleet={true} />);
expect(component).toMatchSnapshot();
});
test('renders with canAccessFleet false', () => {
const component = shallow(<NoDataCard canAccessFleet={false} />);
expect(component).toMatchSnapshot();
});
describe('props', () => {
test('button', () => {
const component = shallow(<NoDataCard button="Button" canAccessFleet={true} />);
expect(component).toMatchSnapshot();
});
test('href', () => {
const component = shallow(<NoDataCard canAccessFleet={true} href={'some path'} />);
expect(component).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,136 @@
/*
* 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, { MouseEventHandler, ReactNode } from 'react';
import {
EuiButton,
EuiCard,
EuiScreenReaderOnly,
EuiTextColor,
EuiCardProps,
EuiImage,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { NoDataCardStyles } from './no_data_card.styles';
import ElasticAgentCardIllustration from './assets/elastic_agent_card.svg';
export type Props = Partial<
Omit<EuiCardProps, 'layout' | 'isDisabled' | 'button' | 'onClick' | 'description'>
> & {
/**
* 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;
/** Remapping `onClick` to any element */
onClick?: MouseEventHandler<HTMLElement>;
/**
* Description for the card;
* If not provided, the default will be used
*/
description?: string | ReactNode;
/** Category to auto-select within Fleet */
category?: string;
/** True if the person has permission to access Fleet, false otherwise */
canAccessFleet?: boolean;
};
const noPermissionTitle = i18n.translate('sharedUXPackages.card.noData.noPermission.title', {
defaultMessage: `Contact your administrator`,
});
const noPermissionDescription = i18n.translate(
'sharedUXPackages.card.noData.noPermission.description',
{
defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`,
}
);
const defaultTitle = i18n.translate('sharedUXPackages.card.noData.title', {
defaultMessage: 'Add Elastic Agent',
});
const defaultDescription = i18n.translate('sharedUXPackages.card.noData.description', {
defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`,
});
const Image = () => (
<EuiImage
size="fullWidth"
style={{
width: 'max(100%, 360px)',
height: 240,
objectFit: 'cover',
background: 'aliceblue',
}}
url={ElasticAgentCardIllustration}
alt=""
/>
);
/**
* Creates a specific NoDataCard pointing users to Integrations when `canAccessFleet`
*/
export const NoDataCard = ({
title: titleProp,
description: descriptionProp,
canAccessFleet,
button,
...props
}: Props) => {
const styles = NoDataCardStyles();
const footer = () => {
// Don't render the footer action if disabled
if (!canAccessFleet) {
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 || titleProp || defaultTitle}</EuiButton>;
};
const title = () => {
if (!canAccessFleet) {
return <EuiTextColor color="default">{noPermissionTitle}</EuiTextColor>;
}
return (
<EuiScreenReaderOnly>
<span>{titleProp || defaultTitle}</span>
</EuiScreenReaderOnly>
);
};
const description = () => {
if (!canAccessFleet) {
return <EuiTextColor color="default">{noPermissionDescription}</EuiTextColor>;
}
return descriptionProp || defaultDescription;
};
return (
<EuiCard
css={styles}
paddingSize="l"
title={title()}
description={description()}
footer={footer()}
isDisabled={!canAccessFleet}
image={<Image />}
{...props}
/>
);
};

View file

@ -0,0 +1,45 @@
/*
* 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 { Params, getStoryArgTypes, getStoryServices } from './mocks';
import { NoDataCard as Component } from './no_data_card.component';
import { NoDataCard as ConnectedComponent } from './no_data_card';
import { NoDataCardProvider } from './services';
import mdx from '../README.mdx';
export default {
title: 'No Data/Card',
description: 'A solution-specific wrapper around `EuiCard`, to be used on `NoData` page',
parameters: {
docs: {
page: mdx,
},
},
};
const argTypes = getStoryArgTypes();
export const NoDataCard = (params: Params) => {
return (
<NoDataCardProvider {...getStoryServices(params)}>
<ConnectedComponent {...params} />
</NoDataCardProvider>
);
};
NoDataCard.argTypes = argTypes;
export const NoDataCardComponent = (params: Params) => {
return <Component {...params} />;
};
NoDataCardComponent.argTypes = argTypes;

View file

@ -6,11 +6,23 @@
* Side Public License, v 1.
*/
import { render } from 'enzyme';
import { render as enzymeRender } from 'enzyme';
import React from 'react';
import { NoDataCard } from './no_data_card';
import { NoDataCardProvider } from './services';
const services = {
addBasePath: (path: string) => path,
navigateToUrl: () => {},
};
describe('NoDataCard', () => {
const render = (element: React.ReactElement, canAccessFleet: boolean = true) =>
enzymeRender(
<NoDataCardProvider {...{ canAccessFleet, ...services }}>{element}</NoDataCardProvider>
);
test('renders', () => {
const component = render(<NoDataCard title="Card title" description="Description" />);
expect(component).toMatchSnapshot();
@ -31,14 +43,10 @@ describe('NoDataCard', () => {
expect(component).toMatchSnapshot();
});
test('isDisabled', () => {
test('no access to Fleet', () => {
const component = render(
<NoDataCard
isDisabled={true}
button="Button"
title="Card title"
description="Description"
/>
<NoDataCard button="Button" title="Card title" description="Description" />,
false
);
expect(component).toMatchSnapshot();
});

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, { useMemo } from 'react';
import { RedirectAppLinksContainer } from '@kbn/shared-ux-link-redirect-app';
import { NoDataCard as Component, Props as ComponentProps } from './no_data_card.component';
import { useServices } from './services';
export type Props = Omit<ComponentProps, 'canAccessFleet'>;
export const NoDataCard = ({ href: srcHref, category, description, ...props }: Props) => {
const { canAccessFleet, addBasePath } = useServices();
const href = useMemo(() => {
if (srcHref) {
return srcHref;
}
// TODO: get this URL from a locator
const prefix = '/app/integrations/browse';
if (category) {
return addBasePath(`${prefix}/${category}`);
}
return addBasePath(prefix);
}, [addBasePath, srcHref, category]);
return (
<RedirectAppLinksContainer>
<Component {...{ ...props, href, canAccessFleet, description }} />
</RedirectAppLinksContainer>
);
};

View file

@ -0,0 +1,97 @@
/*
* 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, { FC, useContext } from 'react';
import {
RedirectAppLinksServices,
RedirectAppLinksKibanaDependencies,
RedirectAppLinksProvider,
RedirectAppLinksKibanaProvider,
} from '@kbn/shared-ux-link-redirect-app';
/**
* A list of services that are consumed by this component.
*/
interface Services {
addBasePath: (path: string) => string;
canAccessFleet: boolean;
}
const Context = React.createContext<Services | null>(null);
/**
* Services that are consumed by this component and its dependencies.
*/
export type NoDataCardServices = Services & RedirectAppLinksServices;
/**
* A Context Provider that provides services to the component and its dependencies.
*/
export const NoDataCardProvider: FC<NoDataCardServices> = ({ children, ...services }) => {
const { addBasePath, canAccessFleet } = services;
return (
<Context.Provider value={{ addBasePath, canAccessFleet }}>
<RedirectAppLinksProvider {...services}>{children}</RedirectAppLinksProvider>
</Context.Provider>
);
};
interface KibanaDependencies {
coreStart: {
http: {
basePath: {
prepend: (path: string) => string;
};
};
application: {
capabilities: {
navLinks: Record<string, boolean>;
};
};
};
}
/**
* An interface containing a collection of Kibana plugins and services required to
* render this component as well as its dependencies.
*/
export type NoDataCardKibanaDependencies = KibanaDependencies & RedirectAppLinksKibanaDependencies;
/**
* Kibana-specific Provider that maps dependencies to services.
*/
export const NoDataCardKibanaProvider: FC<NoDataCardKibanaDependencies> = ({
children,
...dependencies
}) => {
const value: Services = {
addBasePath: dependencies.coreStart.http.basePath.prepend,
canAccessFleet: dependencies.coreStart.application.capabilities.navLinks.integrations,
};
return (
<Context.Provider {...{ value }}>
<RedirectAppLinksKibanaProvider {...dependencies}>{children}</RedirectAppLinksKibanaProvider>
</Context.Provider>
);
};
/**
* React hook for accessing pre-wired services.
*/
export function useServices() {
const context = useContext(Context);
if (!context) {
throw new Error(
'NoDataCard Context is missing. Ensure your component or React root is wrapped with NoDataCardContext.'
);
}
return context;
}

View file

@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"rootDir": "src",
"stripInternal": false,
"types": [
"jest",
"node",
"react",
"@emotion/react/types/css-prop",
"@kbn/ambient-ui-types"
]
},
"include": [
"src/**/*"
]
}

View file

@ -10,16 +10,27 @@ export { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_li
export { RedirectAppLinks as RedirectAppLinksComponent } from './redirect_app_links.component';
export { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services';
export type {
Services as RedirectAppLinksServices,
KibanaDependencies as RedirectAppLinksKibanaDependencies,
} from './services';
export {
getMockServices as getRedirectAppLinksMockServices,
getStoryArgTypes as getRedirectAppLinksStoryArgTypes,
getStoryServices as getRedirectAppLinksStoryServices,
} from './mocks';
import React, { FC } from 'react';
import { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links';
import {
Services,
KibanaServices,
KibanaDependencies,
RedirectAppLinksKibanaProvider,
RedirectAppLinksProvider,
} from './services';
const isKibanaContract = (services: any): services is KibanaServices => {
const isKibanaContract = (services: any): services is KibanaDependencies => {
return typeof services.coreStart !== 'undefined';
};
@ -28,12 +39,22 @@ const isKibanaContract = (services: any): services is KibanaServices => {
* `RedirectAppLinksKibanaProvider` based on the services provided, creating a single component
* with which consumers can wrap their components or solutions.
*/
export const RedirectAppLinks: FC<Services | KibanaServices> = ({ children, ...services }) => {
export const RedirectAppLinks: FC<Services | KibanaDependencies> = ({ children, ...services }) => {
const container = <RedirectAppLinksContainer>{children}</RedirectAppLinksContainer>;
return isKibanaContract(services) ? (
<RedirectAppLinksKibanaProvider {...services}>{container}</RedirectAppLinksKibanaProvider>
) : (
<RedirectAppLinksProvider {...services}>{container}</RedirectAppLinksProvider>
if (isKibanaContract(services)) {
const { coreStart } = services;
return (
<RedirectAppLinksKibanaProvider {...{ coreStart }}>
{container}
</RedirectAppLinksKibanaProvider>
);
}
const { navigateToUrl, currentAppId } = services;
return (
<RedirectAppLinksProvider {...{ currentAppId, navigateToUrl }}>
{container}
</RedirectAppLinksProvider>
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 { Services } from './services';
/**
* Parameters drawn from the Storybook arguments collection that customize a component story.
*/
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
/**
* Returns Storybook-compatible service abstractions for the `NoDataCard` Provider.
*/
export const getStoryServices = () => {
const services: Services = {
navigateToUrl: action('navigateToUrl'),
currentAppId: 'currentAppId',
};
return services;
};
/**
* Returns the Storybook arguments for `NoDataCard`, for its stories and for
* consuming component stories.
*/
export const getStoryArgTypes = () => ({});
/**
* Returns the Jest-compatible service abstractions for the `NoDataCard` Provider.
*/
export const getMockServices = () => {
const services: Services = {
navigateToUrl: jest.fn(),
currentAppId: 'currentAppId',
};
return services;
};

View file

@ -29,12 +29,7 @@ export interface Props extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>,
* </RedirectAppLinks>
* ```
*/
export const RedirectAppLinks: FC<Props> = ({
children,
navigateToUrl,
currentAppId,
...otherProps
}) => {
export const RedirectAppLinks: FC<Props> = ({ children, navigateToUrl, currentAppId }) => {
const containerRef = useRef<HTMLDivElement>(null);
const handleClick: MouseEventHandler<HTMLDivElement> = useCallback(
@ -50,7 +45,7 @@ export const RedirectAppLinks: FC<Props> = ({
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div {...otherProps} ref={containerRef} onClick={handleClick}>
<div ref={containerRef} onClick={handleClick}>
{children}
</div>
);

View file

@ -10,11 +10,12 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { action } from '@storybook/addon-actions';
import { RedirectAppLinks } from '.';
import { RedirectAppLinks as Component } from '.';
import { getStoryArgTypes, getStoryServices } from './mocks';
import mdx from '../README.mdx';
export default {
title: 'Redirect App Links',
title: 'Link',
description:
'An "area of effect" component which intercepts clicks on anchor elements and redirects them to Kibana solutions without a page refresh.',
parameters: {
@ -24,16 +25,10 @@ export default {
},
};
export const Component = () => {
const navigateToUrl = async (url: string) => {
action('navigateToUrl')(url);
};
const currentAppId = 'abc123';
export const RedirectAppLinks = () => {
return (
<>
<RedirectAppLinks {...{ currentAppId, navigateToUrl }}>
<Component {...getStoryServices()}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
@ -54,7 +49,7 @@ export const Component = () => {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</RedirectAppLinks>
</Component>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
@ -69,3 +64,5 @@ export const Component = () => {
</>
);
};
RedirectAppLinks.argTypes = getStoryArgTypes();

View file

@ -6,15 +6,10 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { FC } from 'react';
import { useServices } from './services';
import {
RedirectAppLinks as Component,
Props as ComponentProps,
} from './redirect_app_links.component';
type Props = Omit<ComponentProps, 'navigateToUrl' | 'currentAppId'>;
import { RedirectAppLinks as Component } from './redirect_app_links.component';
/**
* A service-enabled component that provides Kibana-specific functionality to the `RedirectAppLinks`
@ -27,4 +22,6 @@ type Props = Omit<ComponentProps, 'navigateToUrl' | 'currentAppId'>;
* </RedirectAppLinks>
* ```
*/
export const RedirectAppLinks = (props: Props) => <Component {...useServices()} {...props} />;
export const RedirectAppLinks: FC<{}> = ({ children }) => (
<Component {...useServices()}>{children}</Component>
);

View file

@ -25,8 +25,9 @@ const RedirectAppLinksContext = React.createContext<Services | null>(null);
* Contextual services Provider.
*/
export const RedirectAppLinksProvider: FC<Services> = ({ children, ...services }) => {
const { navigateToUrl, currentAppId } = services;
return (
<RedirectAppLinksContext.Provider value={{ ...services }}>
<RedirectAppLinksContext.Provider value={{ navigateToUrl, currentAppId }}>
{children}
</RedirectAppLinksContext.Provider>
);
@ -35,7 +36,7 @@ export const RedirectAppLinksProvider: FC<Services> = ({ children, ...services }
/**
* Kibana-specific contextual services to be adapted for this component.
*/
export interface KibanaServices {
export interface KibanaDependencies {
coreStart: {
application: {
currentAppId$: Observable<string | undefined>;
@ -47,7 +48,7 @@ export interface KibanaServices {
/**
* Kibana-specific contextual services Provider.
*/
export const RedirectAppLinksKibanaProvider: FC<KibanaServices> = ({ children, coreStart }) => {
export const RedirectAppLinksKibanaProvider: FC<KibanaDependencies> = ({ children, coreStart }) => {
const { navigateToUrl, currentAppId$ } = coreStart.application;
const currentAppId = useObservable(currentAppId$, undefined);

View file

@ -16,7 +16,7 @@ import mdx from '../README.mdx';
import { Params, getStoryArgTypes, getStoryServices } from './mocks';
export default {
title: 'No Data/Analytics',
title: 'No Data/Analytics Page',
description: 'An Analytics-specific version of KibanaNoDataPage.',
parameters: {
docs: {

View file

@ -16,7 +16,7 @@ import { KibanaNoDataPageProvider } from './services';
import mdx from '../README.mdx';
export default {
title: 'No Data/Kibana',
title: 'No Data/Kibana Page',
description: 'A component to display when there is no data available',
parameters: {
docs: {

View file

@ -14,6 +14,12 @@ import {
getNoDataViewsPromptMockServices,
} from '@kbn/shared-ux-prompt-no-data-views';
import {
getNoDataCardMockServices,
getNoDataCardStoryArgTypes,
getNoDataCardStoryServices,
} from '@kbn/shared-ux-card-no-data';
import { KibanaNoDataPageServices } from './services';
// TODO: clintandrewhall - this looks (and is) a bit complicated because the No Data View
@ -32,6 +38,8 @@ export const getStoryServices = (params: StoryParams) => {
const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } =
getNoDataViewsPromptStorybookServices(params);
const { addBasePath, canAccessFleet } = getNoDataCardStoryServices(params);
// Workaround to leverage the services package.
const { application, data, docLinks, editors, http, permissions, platform } =
servicesFactory(params);
@ -47,6 +55,8 @@ export const getStoryServices = (params: StoryParams) => {
canCreateNewDataView,
dataViewsDocLink,
openDataViewEditor,
addBasePath,
canAccessFleet,
};
return services;
@ -75,6 +85,7 @@ export const getStoryArgTypes = () => ({
defaultValue: false,
},
...getNoDataViewsPromptStoryArgTypes(),
...getNoDataCardStoryArgTypes(),
});
/**
@ -84,6 +95,8 @@ export const getMockServices = (params?: MockServicesFactoryParams) => {
const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } =
getNoDataViewsPromptMockServices();
const { addBasePath, canAccessFleet } = getNoDataCardMockServices();
const { application, data, docLinks, editors, http, permissions, platform } =
mockServicesFactory(params);
@ -98,6 +111,8 @@ export const getMockServices = (params?: MockServicesFactoryParams) => {
canCreateNewDataView,
dataViewsDocLink,
openDataViewEditor,
addBasePath,
canAccessFleet,
};
return services;

View file

@ -13,6 +13,8 @@ import {
NoDataViewsPromptKibanaProvider,
} from '@kbn/shared-ux-prompt-no-data-views';
import { NoDataCardProvider, NoDataCardKibanaProvider } from '@kbn/shared-ux-card-no-data';
import { LegacyServicesProvider, getLegacyServices } from './legacy_services';
/**
@ -85,7 +87,9 @@ export const KibanaNoDataPageProvider: FC<KibanaNoDataPageServices> = ({
}) => (
<KibanaNoDataPageContext.Provider value={services}>
<NoDataViewsPromptProvider {...services}>
<LegacyServicesProvider {...getLegacyServices(services)}>{children}</LegacyServicesProvider>
<NoDataCardProvider {...services}>
<LegacyServicesProvider {...getLegacyServices(services)}>{children}</LegacyServicesProvider>
</NoDataCardProvider>
</NoDataViewsPromptProvider>
</KibanaNoDataPageContext.Provider>
);
@ -159,7 +163,9 @@ export const KibanaNoDataPageKibanaProvider: FC<KibanaNoDataPageKibanaDependenci
return (
<KibanaNoDataPageContext.Provider value={value}>
<NoDataViewsPromptKibanaProvider {...dependencies}>
<LegacyServicesProvider {...getLegacyServices(value)}>{children}</LegacyServicesProvider>
<NoDataCardKibanaProvider {...dependencies}>
<LegacyServicesProvider {...getLegacyServices(value)}>{children}</LegacyServicesProvider>
</NoDataCardKibanaProvider>
</NoDataViewsPromptKibanaProvider>
</KibanaNoDataPageContext.Provider>
);

View file

@ -5336,11 +5336,10 @@
"share.urlService.redirect.RedirectManager.missingParamLocator": "ID du localisateur non spécifié. Spécifiez le paramètre de recherche \"l\" dans l'URL ; ce devrait être un ID de localisateur existant.",
"share.urlService.redirect.RedirectManager.missingParamParams": "Paramètres du localisateur non spécifiés. Spécifiez le paramètre de recherche \"p\" dans l'URL ; ce devrait être un objet sérialisé JSON des paramètres du localisateur.",
"share.urlService.redirect.RedirectManager.missingParamVersion": "Version des paramètres du localisateur non spécifiée. Spécifiez le paramètre de recherche \"v\" dans l'URL ; ce devrait être la version de Kibana au moment de la génération des paramètres du localisateur.",
"sharedUXComponents.noDataPage.elasticAgentCard.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "Cette intégration n'est pas encore activée. Votre administrateur possède les autorisations requises pour lactiver.",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "Contactez votre administrateur",
"sharedUXComponents.noDataPage.elasticAgentCard.title": "Ajouter Elastic Agent",
"sharedUXComponents.pageTemplate.noDataCard.description": "Continuer sans collecter de données",
"sharedUXPackages.card.noData.description": "Utilisez Elastic Agent pour collecter de manière simple et unifiée les données de vos machines.",
"sharedUXPackages.card.noData.noPermission.description": "Cette intégration n'est pas encore activée. Votre administrateur possède les autorisations requises pour lactiver.",
"sharedUXPackages.card.noData.noPermission.title": "Contactez votre administrateur",
"sharedUXPackages.card.noData.title": "Ajouter Elastic Agent",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "Ajouter depuis la bibliothèque",
"sharedUXPackages.noDataViewsPrompt.learnMore": "Envie d'en savoir plus ?",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "Lisez les documents",

View file

@ -5438,11 +5438,10 @@
"share.urlService.redirect.RedirectManager.missingParamLocator": "ロケーターIDが指定されていません。URLで「l」検索パラメーターを指定します。これは既存のロケーターIDにしてください。",
"share.urlService.redirect.RedirectManager.missingParamParams": "ロケーターパラメーターが指定されていません。URLで「p」検索パラメーターを指定します。これはロケーターパラメーターのJSONシリアル化オブジェクトにしてください。",
"share.urlService.redirect.RedirectManager.missingParamVersion": "ロケーターパラメーターバージョンが指定されていません。URLで「v」検索パラメーターを指定します。これはロケーターパラメーターが生成されたときのKibanaのリリースバージョンです。",
"sharedUXComponents.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "この統合はまだ有効ではありません。管理者にはオンにするために必要なアクセス権があります。",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "管理者にお問い合わせください",
"sharedUXComponents.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加",
"sharedUXComponents.pageTemplate.noDataCard.description": "データを収集せずに続行",
"sharedUXPackages.card.noData.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。",
"sharedUXPackages.card.noData.noPermission.description": "この統合はまだ有効ではありません。管理者にはオンにするために必要なアクセス権があります。",
"sharedUXPackages.card.noData.noPermission.title": "管理者にお問い合わせください",
"sharedUXPackages.card.noData.title": "Elasticエージェントの追加",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "ライブラリから追加",
"sharedUXPackages.noDataViewsPrompt.learnMore": "詳細について",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "<b>ドキュメント</b><b>を読む</b>",

View file

@ -5449,11 +5449,10 @@
"share.urlService.redirect.RedirectManager.missingParamLocator": "未指定定位器 ID。在 URL 中指定“l”搜索参数其应为现有定位器 ID。",
"share.urlService.redirect.RedirectManager.missingParamParams": "定位器参数未指定。在 URL 中指定“p”搜索参数其应为定位器参数的 JSON 序列化对象。",
"share.urlService.redirect.RedirectManager.missingParamVersion": "定位器参数版本未指定。在 URL 中指定“v”搜索参数其应为生成定位器参数时 Kibana 的版本。",
"sharedUXComponents.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.description": "尚未启用此集成。您的管理员具有打开它所需的权限。",
"sharedUXComponents.noDataPage.elasticAgentCard.noPermission.title": "请联系您的管理员",
"sharedUXComponents.noDataPage.elasticAgentCard.title": "添加 Elastic 代理",
"sharedUXComponents.pageTemplate.noDataCard.description": "继续,而不收集数据",
"sharedUXPackages.card.noData.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。",
"sharedUXPackages.card.noData.noPermission.description": "尚未启用此集成。您的管理员具有打开它所需的权限。",
"sharedUXPackages.card.noData.noPermission.title": "请联系您的管理员",
"sharedUXPackages.card.noData.title": "添加 Elastic 代理",
"sharedUXPackages.buttonToolbar.buttons.addFromLibrary.libraryButtonLabel": "从库中添加",
"sharedUXPackages.noDataViewsPrompt.learnMore": "希望了解详情?",
"sharedUXPackages.noDataViewsPrompt.readDocumentation": "阅读文档",

View file

@ -3351,6 +3351,10 @@
version "0.0.0"
uid ""
"@kbn/shared-ux-card-no-data@link:bazel-bin/packages/shared-ux/card/no_data":
version "0.0.0"
uid ""
"@kbn/shared-ux-components@link:bazel-bin/packages/kbn-shared-ux-components":
version "0.0.0"
uid ""
@ -6722,6 +6726,10 @@
version "0.0.0"
uid ""
"@types/kbn__shared-ux-card-no-data@link:bazel-bin/packages/shared-ux/card/no_data/npm_module_types":
version "0.0.0"
uid ""
"@types/kbn__shared-ux-components@link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types":
version "0.0.0"
uid ""