[Shared UX][packages] Move No Data Page components; model service composition (#133593)

* [Shared UX] Move No Data Page components to packages

* Fix type export.

* Fix Kibana dependency miss; exported type

* Address feedback; add docs

* More updates after merge

* Move comment

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2022-06-13 19:19:02 -05:00 committed by GitHub
parent cde209b23a
commit 8882a50a34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 923 additions and 693 deletions

View file

@ -203,6 +203,7 @@
"@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",
"@kbn/shared-ux-page-kibana-no-data": "link:bazel-bin/packages/shared-ux/page/kibana_no_data",
"@kbn/shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views",
"@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services",
"@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook",
@ -729,6 +730,7 @@
"@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",
"@types/kbn__shared-ux-page-kibana-no-data": "link:bazel-bin/packages/shared-ux/page/kibana_no_data/npm_module_types",
"@types/kbn__shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/npm_module_types",
"@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types",
"@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types",

View file

@ -136,6 +136,7 @@ filegroup(
"//packages/shared-ux/button/exit_full_screen:build",
"//packages/shared-ux/link/redirect_app:build",
"//packages/shared-ux/page/analytics_no_data:build",
"//packages/shared-ux/page/kibana_no_data:build",
"//packages/shared-ux/prompt/no_data_views:build",
],
)
@ -256,6 +257,7 @@ filegroup(
"//packages/shared-ux/button/exit_full_screen: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",
"//packages/shared-ux/prompt/no_data_views:build_types",
],
)

View file

@ -1,23 +0,0 @@
---
id: sharedUX/Components/KibanaNoDataPage
slug: /shared-ux-components/kibana_no_data_page
title: Kibana No Data Page
summary: A page to be displayed when there is no data in Elasticsearch, or no data views
tags: ['shared-ux', 'component']
date: 2022-04-20
---
## Description
Many plugins display "no data" page, either when there is no data in Elasticsearch, or there haven't been any data views created yet. This component is meant
to be used in those scenarios. It displays an appropriate message to the user and facilitate addition of integrations and creation of data views.
## Component: `KibanaNoDataPage`
- uses `hasUserDataView` and `hasData` API from `HasData` service in `data_views` plugin to check for existence of data / data views
- uses `onDataViewCreated` callback to be called once the data view has been created
- receives (noDataConfig)[https://github.com/elastic/kibana/blob/main/packages/kbn-shared-ux-components/src/page_template/no_data_page/types.ts] as configuration for the page in case of no data
- needs to be wrapped in `ServicesContext` provided by the start contract of the `shared_ux` plugin to be used
## EUI Promotion Status
This component is not currently considered for promotion to EUI.

View file

@ -1,93 +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 { action } from '@storybook/addon-actions';
import React from 'react';
import { servicesFactory, DataServiceFactoryConfig } from '@kbn/shared-ux-storybook';
import { SharedUxServicesProvider } from '@kbn/shared-ux-services';
import mdx from './kibana_no_data_page.mdx';
import { NoDataPageProps } from '../page_template';
import { KibanaNoDataPage } from './kibana_no_data_page';
export default {
title: 'No Data/Kibana No Data Page',
description: 'A component to display when there is no data available',
parameters: {
docs: {
page: mdx,
},
},
};
const noDataConfig = {
solution: 'Analytics',
logo: 'logoKibana',
action: {
elasticAgent: {
title: 'Add Integrations',
},
},
docsLink: 'http://docs.elastic.dev',
};
type Params = Pick<NoDataPageProps, 'solution' | 'logo'> & DataServiceFactoryConfig;
export const PureComponent = (params: Params) => {
const { solution, logo, hasESData, hasUserDataView } = params;
const serviceParams = { hasESData, hasUserDataView, hasDataViews: false };
const services = servicesFactory({ ...serviceParams, hasESData, hasUserDataView });
return (
<SharedUxServicesProvider {...services}>
<KibanaNoDataPage
onDataViewCreated={action('onDataViewCreated')}
noDataConfig={{ ...noDataConfig, solution, logo }}
/>
</SharedUxServicesProvider>
);
};
export const PureComponentLoadingState = () => {
const dataCheck = () => new Promise<boolean>((resolve, reject) => {});
const services = {
...servicesFactory({ hasESData: false, hasUserDataView: false, hasDataViews: false }),
data: {
hasESData: dataCheck,
hasUserDataView: dataCheck,
hasDataView: dataCheck,
},
};
return (
<SharedUxServicesProvider {...services}>
<KibanaNoDataPage
onDataViewCreated={action('onDataViewCreated')}
noDataConfig={noDataConfig}
/>
</SharedUxServicesProvider>
);
};
PureComponent.argTypes = {
solution: {
control: 'text',
defaultValue: 'Observability',
},
logo: {
control: { type: 'radio' },
options: ['logoElastic', 'logoKibana', 'logoCloud', undefined],
defaultValue: undefined,
},
hasESData: {
control: 'boolean',
defaultValue: false,
},
hasUserDataView: {
control: 'boolean',
defaultValue: false,
},
};

View file

@ -6,27 +6,11 @@
* Side Public License, v 1.
*/
import React from 'react';
import { withSuspense } from '@kbn/shared-ux-utility';
export { ToolbarButton, IconButtonGroup, AddFromLibraryButton, ToolbarPopover } from './toolbar';
export { KibanaPageTemplateSolutionNav } from './page_template/solution_nav';
export type { KibanaPageTemplateProps } from './page_template';
export { KibanaPageTemplate } from './page_template';
/**
* A `KibanaNoDataPage` component, with service hooks. Consumers should use `React.Suspennse` or the
* `withSuspense` HOC to load this component.
*/
export const KibanaNoDataPageLazy = React.lazy(() =>
import('./empty_state').then(({ KibanaNoDataPage }) => ({
default: KibanaNoDataPage,
}))
);
// TODO: clintandrewhall - NoDataPageProps is a temporary addition until it is split into its own package
export type { KibanaPageTemplateProps, NoDataPageProps } from './page_template';
/**
* A `KibanaNoDataPage` component. The component is wrapped by the `withSuspense` HOC.
* This component can be used directly by consumers and will load the `KibanaNoDataPageLazy` lazily with
* a predefined fallback and error boundary.
*/
export const KibanaNoDataPage = withSuspense(KibanaNoDataPageLazy);
// TODO: clintandrewhall - NoDataConfigPage is a temporary addition until it is split into its own package
export { KibanaPageTemplate, NoDataConfigPage } from './page_template';

View file

@ -8,6 +8,7 @@
export type { ServiceFactory, SharedUxServices, SharedUxServicesContext } from './types';
export type {
MockServicesFactoryParams,
SharedUxApplicationService,
SharedUxDocLinksService,
SharedUxEditorsService,

View file

@ -13,6 +13,7 @@ export type { SharedUxHttpService } from './http';
export type { SharedUxUserPermissionsService } from './permissions';
export type { SharedUxPlatformService } from './platform';
export type { SharedUxDataService } from './data';
export type { MockServicesFactoryParams } from './mock';
export { mockServicesFactory, mockServiceFactories } from './mock';
export { stubServicesFactory, stubServiceFactories } from './stub';

View file

@ -39,11 +39,9 @@ NPM_MODULE_EXTRA_FILES = [
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//react",
"@npm//rxjs",
"@npm//@testing-library",
"//packages/kbn-i18n",
"//packages/kbn-shared-ux-services",
"//packages/kbn-shared-ux-components",
"//packages/kbn-shared-ux-storybook"
"//packages/shared-ux/page/kibana_no_data",
]
# In this array place dependencies necessary to build the types, which will include the
@ -60,9 +58,7 @@ TYPES_DEPS = [
"@npm//@types/jest",
"@npm//@types/react",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-shared-ux-services:npm_module_types",
"//packages/kbn-shared-ux-storybook:npm_module_types",
"//packages/kbn-shared-ux-components:npm_module_types",
"//packages/shared-ux/page/kibana_no_data:npm_module_types",
"//packages/kbn-ambient-ui-types",
]

View file

@ -1,15 +1,22 @@
---
id: sharedUX/Components/AnalyticsNoDataPage
slug: /shared-ux/components/analytics-no-data-page
id: sharedUX/Page/AnalyticsNoDataPage
slug: /shared-ux/page/analytics-no-data-page
title: Analytics "No Data" Page
summary: An entire page that can be displayed when Kibana "has no data", specifically for Analytics.
tags: ['shared-ux', 'component']
date: 2021-12-28
---
## Description
This is an Analytics-specific version of `KibanaNoDataPage`, which defaults most of the fields appropriate for Analytics solutions.
This is an Analytics-specific version of `KibanaNoDataPage`, which defaults most of the fields to give a consistent set of terms for Analytics solutions.
## API
| Export | Description |
|---|---|
| `AnalyticsNoDataPageProvider` | Provides contextual services to `AnalyticsNoDataPage`. |
| `AnalyticsNoDataPageKibanaProvider` | Maps Kibana dependencies to provide contextual services to `AnalyticsNoDataPage`. |
| `AnalyticsNoDataPage` | Uses a `Provider` to access contextual services to populate props on the `AnalyticsNoDataPageComponent`. |
| `AnalyticsNoDataPageComponent` | The pure component. |
## EUI Promotion Status

View file

@ -1,276 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnalyticsNoDataPageComponent renders correctly 1`] = `
<AnalyticsNoDataPage
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,
}
}
kibanaGuideDocLink="http://www.test.com"
onDataViewCreated={[MockFunction]}
>
<ForwardRef
noDataConfig={
Object {
"action": Object {
"elasticAgent": Object {
"data-test-subj": "kbnOverviewAddIntegrations",
"description": "Use Elastic Agent to collect data and build out Analytics solutions.",
"title": "Add integrations",
},
},
"docsLink": "http://www.test.com",
"logo": "logoKibana",
"pageTitle": "Welcome to Analytics!",
"solution": "Analytics",
}
}
onDataViewCreated={[MockFunction]}
>
<EuiErrorBoundary>
<Suspense
fallback={<Fallback />}
>
<Fallback>
<div
css={
Object {
"map": undefined,
"name": "1q0vk5q",
"next": undefined,
"styles": "
text-align: center;
",
"toString": [Function],
}
}
>
<EuiLoadingSpinner>
<span
aria-label="Loading"
css="unknown styles"
role="progressbar"
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"155ohbx-euiLoadingSpinner-m": true,
"animation-c24ia8": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-emotion="css"
data-s=""
>
.css-155ohbx-euiLoadingSpinner-m{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;display:inline-block;border-radius:50%;border:2px solid #D3DAE6;border-color:#07C #D3DAE6 #D3DAE6 #D3DAE6;width:16px;height:16px;border-width:calc(1px * 1.5);}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2xvYWRpbmcvbG9hZGluZ19zcGlubmVyLnN0eWxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE0RFUiLCJmaWxlIjoiLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvbG9hZGluZy9sb2FkaW5nX3NwaW5uZXIuc3R5bGVzLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIENvcHlyaWdodCBFbGFzdGljc2VhcmNoIEIuVi4gYW5kL29yIGxpY2Vuc2VkIHRvIEVsYXN0aWNzZWFyY2ggQi5WLiB1bmRlciBvbmVcbiAqIG9yIG1vcmUgY29udHJpYnV0b3IgbGljZW5zZSBhZ3JlZW1lbnRzLiBMaWNlbnNlZCB1bmRlciB0aGUgRWxhc3RpYyBMaWNlbnNlXG4gKiAyLjAgYW5kIHRoZSBTZXJ2ZXIgU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdFxuICogaW4gY29tcGxpYW5jZSB3aXRoLCBhdCB5b3VyIGVsZWN0aW9uLCB0aGUgRWxhc3RpYyBMaWNlbnNlIDIuMCBvciB0aGUgU2VydmVyXG4gKiBTaWRlIFB1YmxpYyBMaWNlbnNlLCB2IDEuXG4gKi9cblxuaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9yZWFjdCc7XG5pbXBvcnQgeyBfRXVpVGhlbWVTaXplLCBldWlDYW5BbmltYXRlIH0gZnJvbSAnLi4vLi4vZ2xvYmFsX3N0eWxpbmcnO1xuaW1wb3J0IHsgVXNlRXVpVGhlbWUgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcyc7XG5pbXBvcnQgeyBFdWlMb2FkaW5nU3Bpbm5lclNpemUgfSBmcm9tICcuL2xvYWRpbmdfc3Bpbm5lcic7XG5cbmNvbnN0IF9sb2FkaW5nU3Bpbm5lciA9IGtleWZyYW1lc2BcbiAgZnJvbSB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgfVxuYDtcblxuY29uc3Qgc3Bpbm5lclNpemVzOiB7XG4gIFtzaXplIGluIEV1aUxvYWRpbmdTcGlubmVyU2l6ZV06IF9FdWlUaGVtZVNpemU7XG59ID0ge1xuICBzOiAnbScsXG4gIG06ICdiYXNlJyxcbiAgbDogJ2wnLFxuICB4bDogJ3hsJyxcbiAgeHhsOiAneHhsJyxcbn07XG5cbmNvbnN0IHNwaW5uZXJDb2xvcnMgPSAobWFpbjogc3RyaW5nLCBoaWdobGlnaHQ6IHN0cmluZykgPT4ge1xuICByZXR1cm4gYCR7aGlnaGxpZ2h0fSAke21haW59ICR7bWFpbn0gJHttYWlufWA7XG59O1xuXG5leHBvcnQgY29uc3QgZXVpTG9hZGluZ1NwaW5uZXJTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4ge1xuICByZXR1cm4ge1xuICAgIGV1aUxvYWRpbmdTcGlubmVyOiBjc3NgXG4gICAgICBmbGV4LXNocmluazogMDsgLy8gRW5zdXJlcyBpdCBuZXZlciBzY2FsZXMgZG93biBiZWxvdyBpdHMgaW50ZW5kZWQgc2l6ZVxuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgICAgYm9yZGVyOiAke2V1aVRoZW1lLmJvcmRlci50aGlja307XG4gICAgICBib3JkZXItY29sb3I6ICR7c3Bpbm5lckNvbG9ycyhcbiAgICAgICAgZXVpVGhlbWUuYm9yZGVyLmNvbG9yLFxuICAgICAgICBldWlUaGVtZS5jb2xvcnMucHJpbWFyeVxuICAgICAgKX07XG5cbiAgICAgICR7ZXVpQ2FuQW5pbWF0ZX0ge1xuICAgICAgICBhbmltYXRpb246ICR7X2xvYWRpbmdTcGlubmVyfSAwLjZzIGluZmluaXRlIGxpbmVhcjtcbiAgICAgIH1cbiAgICBgLFxuXG4gICAgLy8gU2l6ZXNcbiAgICBzOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5zXX07XG4gICAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMuc119O1xuICAgICAgYm9yZGVyLXdpZHRoOiBjYWxjKCR7ZXVpVGhlbWUuYm9yZGVyLndpZHRoLnRoaW59ICogMS41KTtcbiAgICBgLFxuICAgIG06IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLm1dfTtcbiAgICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5tXX07XG4gICAgICBib3JkZXItd2lkdGg6IGNhbGMoJHtldWlUaGVtZS5ib3JkZXIud2lkdGgudGhpbn0gKiAxLjUpO1xuICAgIGAsXG4gICAgbDogY3NzYFxuICAgICAgd2lkdGg6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMubF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLmxdfTtcbiAgICBgLFxuICAgIHhsOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy54bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnhsXX07XG4gICAgYCxcbiAgICB4eGw6IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgIGAsXG4gIH07XG59O1xuIl19 */
</style>
<style
data-emotion="css"
data-s=""
>
@media screen and (prefers-reduced-motion: no-preference){.css-155ohbx-euiLoadingSpinner-m{-webkit-animation:animation-c24ia8 0.6s infinite linear;animation:animation-c24ia8 0.6s infinite linear;}}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2xvYWRpbmcvbG9hZGluZ19zcGlubmVyLnN0eWxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE0RFUiLCJmaWxlIjoiLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvbG9hZGluZy9sb2FkaW5nX3NwaW5uZXIuc3R5bGVzLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIENvcHlyaWdodCBFbGFzdGljc2VhcmNoIEIuVi4gYW5kL29yIGxpY2Vuc2VkIHRvIEVsYXN0aWNzZWFyY2ggQi5WLiB1bmRlciBvbmVcbiAqIG9yIG1vcmUgY29udHJpYnV0b3IgbGljZW5zZSBhZ3JlZW1lbnRzLiBMaWNlbnNlZCB1bmRlciB0aGUgRWxhc3RpYyBMaWNlbnNlXG4gKiAyLjAgYW5kIHRoZSBTZXJ2ZXIgU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdFxuICogaW4gY29tcGxpYW5jZSB3aXRoLCBhdCB5b3VyIGVsZWN0aW9uLCB0aGUgRWxhc3RpYyBMaWNlbnNlIDIuMCBvciB0aGUgU2VydmVyXG4gKiBTaWRlIFB1YmxpYyBMaWNlbnNlLCB2IDEuXG4gKi9cblxuaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9yZWFjdCc7XG5pbXBvcnQgeyBfRXVpVGhlbWVTaXplLCBldWlDYW5BbmltYXRlIH0gZnJvbSAnLi4vLi4vZ2xvYmFsX3N0eWxpbmcnO1xuaW1wb3J0IHsgVXNlRXVpVGhlbWUgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcyc7XG5pbXBvcnQgeyBFdWlMb2FkaW5nU3Bpbm5lclNpemUgfSBmcm9tICcuL2xvYWRpbmdfc3Bpbm5lcic7XG5cbmNvbnN0IF9sb2FkaW5nU3Bpbm5lciA9IGtleWZyYW1lc2BcbiAgZnJvbSB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgfVxuYDtcblxuY29uc3Qgc3Bpbm5lclNpemVzOiB7XG4gIFtzaXplIGluIEV1aUxvYWRpbmdTcGlubmVyU2l6ZV06IF9FdWlUaGVtZVNpemU7XG59ID0ge1xuICBzOiAnbScsXG4gIG06ICdiYXNlJyxcbiAgbDogJ2wnLFxuICB4bDogJ3hsJyxcbiAgeHhsOiAneHhsJyxcbn07XG5cbmNvbnN0IHNwaW5uZXJDb2xvcnMgPSAobWFpbjogc3RyaW5nLCBoaWdobGlnaHQ6IHN0cmluZykgPT4ge1xuICByZXR1cm4gYCR7aGlnaGxpZ2h0fSAke21haW59ICR7bWFpbn0gJHttYWlufWA7XG59O1xuXG5leHBvcnQgY29uc3QgZXVpTG9hZGluZ1NwaW5uZXJTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4ge1xuICByZXR1cm4ge1xuICAgIGV1aUxvYWRpbmdTcGlubmVyOiBjc3NgXG4gICAgICBmbGV4LXNocmluazogMDsgLy8gRW5zdXJlcyBpdCBuZXZlciBzY2FsZXMgZG93biBiZWxvdyBpdHMgaW50ZW5kZWQgc2l6ZVxuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgICAgYm9yZGVyOiAke2V1aVRoZW1lLmJvcmRlci50aGlja307XG4gICAgICBib3JkZXItY29sb3I6ICR7c3Bpbm5lckNvbG9ycyhcbiAgICAgICAgZXVpVGhlbWUuYm9yZGVyLmNvbG9yLFxuICAgICAgICBldWlUaGVtZS5jb2xvcnMucHJpbWFyeVxuICAgICAgKX07XG5cbiAgICAgICR7ZXVpQ2FuQW5pbWF0ZX0ge1xuICAgICAgICBhbmltYXRpb246ICR7X2xvYWRpbmdTcGlubmVyfSAwLjZzIGluZmluaXRlIGxpbmVhcjtcbiAgICAgIH1cbiAgICBgLFxuXG4gICAgLy8gU2l6ZXNcbiAgICBzOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5zXX07XG4gICAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMuc119O1xuICAgICAgYm9yZGVyLXdpZHRoOiBjYWxjKCR7ZXVpVGhlbWUuYm9yZGVyLndpZHRoLnRoaW59ICogMS41KTtcbiAgICBgLFxuICAgIG06IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLm1dfTtcbiAgICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5tXX07XG4gICAgICBib3JkZXItd2lkdGg6IGNhbGMoJHtldWlUaGVtZS5ib3JkZXIud2lkdGgudGhpbn0gKiAxLjUpO1xuICAgIGAsXG4gICAgbDogY3NzYFxuICAgICAgd2lkdGg6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMubF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLmxdfTtcbiAgICBgLFxuICAgIHhsOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy54bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnhsXX07XG4gICAgYCxcbiAgICB4eGw6IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgIGAsXG4gIH07XG59O1xuIl19 */
</style>
<style
data-emotion="css"
data-s=""
>
@-webkit-keyframes animation-c24ia8{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(359deg);-moz-transform:rotate(359deg);-ms-transform:rotate(359deg);transform:rotate(359deg);}}
</style>
<style
data-emotion="css"
data-s=""
>
@keyframes animation-c24ia8{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(359deg);-moz-transform:rotate(359deg);-ms-transform:rotate(359deg);transform:rotate(359deg);}}
</style>
<style
data-styled="active"
data-styled-version="5.1.0"
/>
</head>,
"ctr": 4,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.css-155ohbx-euiLoadingSpinner-m{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;display:inline-block;border-radius:50%;border:2px solid #D3DAE6;border-color:#07C #D3DAE6 #D3DAE6 #D3DAE6;width:16px;height:16px;border-width:calc(1px * 1.5);}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2xvYWRpbmcvbG9hZGluZ19zcGlubmVyLnN0eWxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE0RFUiLCJmaWxlIjoiLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvbG9hZGluZy9sb2FkaW5nX3NwaW5uZXIuc3R5bGVzLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIENvcHlyaWdodCBFbGFzdGljc2VhcmNoIEIuVi4gYW5kL29yIGxpY2Vuc2VkIHRvIEVsYXN0aWNzZWFyY2ggQi5WLiB1bmRlciBvbmVcbiAqIG9yIG1vcmUgY29udHJpYnV0b3IgbGljZW5zZSBhZ3JlZW1lbnRzLiBMaWNlbnNlZCB1bmRlciB0aGUgRWxhc3RpYyBMaWNlbnNlXG4gKiAyLjAgYW5kIHRoZSBTZXJ2ZXIgU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdFxuICogaW4gY29tcGxpYW5jZSB3aXRoLCBhdCB5b3VyIGVsZWN0aW9uLCB0aGUgRWxhc3RpYyBMaWNlbnNlIDIuMCBvciB0aGUgU2VydmVyXG4gKiBTaWRlIFB1YmxpYyBMaWNlbnNlLCB2IDEuXG4gKi9cblxuaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9yZWFjdCc7XG5pbXBvcnQgeyBfRXVpVGhlbWVTaXplLCBldWlDYW5BbmltYXRlIH0gZnJvbSAnLi4vLi4vZ2xvYmFsX3N0eWxpbmcnO1xuaW1wb3J0IHsgVXNlRXVpVGhlbWUgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcyc7XG5pbXBvcnQgeyBFdWlMb2FkaW5nU3Bpbm5lclNpemUgfSBmcm9tICcuL2xvYWRpbmdfc3Bpbm5lcic7XG5cbmNvbnN0IF9sb2FkaW5nU3Bpbm5lciA9IGtleWZyYW1lc2BcbiAgZnJvbSB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgfVxuYDtcblxuY29uc3Qgc3Bpbm5lclNpemVzOiB7XG4gIFtzaXplIGluIEV1aUxvYWRpbmdTcGlubmVyU2l6ZV06IF9FdWlUaGVtZVNpemU7XG59ID0ge1xuICBzOiAnbScsXG4gIG06ICdiYXNlJyxcbiAgbDogJ2wnLFxuICB4bDogJ3hsJyxcbiAgeHhsOiAneHhsJyxcbn07XG5cbmNvbnN0IHNwaW5uZXJDb2xvcnMgPSAobWFpbjogc3RyaW5nLCBoaWdobGlnaHQ6IHN0cmluZykgPT4ge1xuICByZXR1cm4gYCR7aGlnaGxpZ2h0fSAke21haW59ICR7bWFpbn0gJHttYWlufWA7XG59O1xuXG5leHBvcnQgY29uc3QgZXVpTG9hZGluZ1NwaW5uZXJTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4ge1xuICByZXR1cm4ge1xuICAgIGV1aUxvYWRpbmdTcGlubmVyOiBjc3NgXG4gICAgICBmbGV4LXNocmluazogMDsgLy8gRW5zdXJlcyBpdCBuZXZlciBzY2FsZXMgZG93biBiZWxvdyBpdHMgaW50ZW5kZWQgc2l6ZVxuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgICAgYm9yZGVyOiAke2V1aVRoZW1lLmJvcmRlci50aGlja307XG4gICAgICBib3JkZXItY29sb3I6ICR7c3Bpbm5lckNvbG9ycyhcbiAgICAgICAgZXVpVGhlbWUuYm9yZGVyLmNvbG9yLFxuICAgICAgICBldWlUaGVtZS5jb2xvcnMucHJpbWFyeVxuICAgICAgKX07XG5cbiAgICAgICR7ZXVpQ2FuQW5pbWF0ZX0ge1xuICAgICAgICBhbmltYXRpb246ICR7X2xvYWRpbmdTcGlubmVyfSAwLjZzIGluZmluaXRlIGxpbmVhcjtcbiAgICAgIH1cbiAgICBgLFxuXG4gICAgLy8gU2l6ZXNcbiAgICBzOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5zXX07XG4gICAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMuc119O1xuICAgICAgYm9yZGVyLXdpZHRoOiBjYWxjKCR7ZXVpVGhlbWUuYm9yZGVyLndpZHRoLnRoaW59ICogMS41KTtcbiAgICBgLFxuICAgIG06IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLm1dfTtcbiAgICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5tXX07XG4gICAgICBib3JkZXItd2lkdGg6IGNhbGMoJHtldWlUaGVtZS5ib3JkZXIud2lkdGgudGhpbn0gKiAxLjUpO1xuICAgIGAsXG4gICAgbDogY3NzYFxuICAgICAgd2lkdGg6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMubF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLmxdfTtcbiAgICBgLFxuICAgIHhsOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy54bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnhsXX07XG4gICAgYCxcbiAgICB4eGw6IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgIGAsXG4gIH07XG59O1xuIl19 */
</style>,
<style
data-emotion="css"
data-s=""
>
@media screen and (prefers-reduced-motion: no-preference){.css-155ohbx-euiLoadingSpinner-m{-webkit-animation:animation-c24ia8 0.6s infinite linear;animation:animation-c24ia8 0.6s infinite linear;}}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2xvYWRpbmcvbG9hZGluZ19zcGlubmVyLnN0eWxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE0RFUiLCJmaWxlIjoiLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvbG9hZGluZy9sb2FkaW5nX3NwaW5uZXIuc3R5bGVzLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIENvcHlyaWdodCBFbGFzdGljc2VhcmNoIEIuVi4gYW5kL29yIGxpY2Vuc2VkIHRvIEVsYXN0aWNzZWFyY2ggQi5WLiB1bmRlciBvbmVcbiAqIG9yIG1vcmUgY29udHJpYnV0b3IgbGljZW5zZSBhZ3JlZW1lbnRzLiBMaWNlbnNlZCB1bmRlciB0aGUgRWxhc3RpYyBMaWNlbnNlXG4gKiAyLjAgYW5kIHRoZSBTZXJ2ZXIgU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdFxuICogaW4gY29tcGxpYW5jZSB3aXRoLCBhdCB5b3VyIGVsZWN0aW9uLCB0aGUgRWxhc3RpYyBMaWNlbnNlIDIuMCBvciB0aGUgU2VydmVyXG4gKiBTaWRlIFB1YmxpYyBMaWNlbnNlLCB2IDEuXG4gKi9cblxuaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9yZWFjdCc7XG5pbXBvcnQgeyBfRXVpVGhlbWVTaXplLCBldWlDYW5BbmltYXRlIH0gZnJvbSAnLi4vLi4vZ2xvYmFsX3N0eWxpbmcnO1xuaW1wb3J0IHsgVXNlRXVpVGhlbWUgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcyc7XG5pbXBvcnQgeyBFdWlMb2FkaW5nU3Bpbm5lclNpemUgfSBmcm9tICcuL2xvYWRpbmdfc3Bpbm5lcic7XG5cbmNvbnN0IF9sb2FkaW5nU3Bpbm5lciA9IGtleWZyYW1lc2BcbiAgZnJvbSB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgfVxuYDtcblxuY29uc3Qgc3Bpbm5lclNpemVzOiB7XG4gIFtzaXplIGluIEV1aUxvYWRpbmdTcGlubmVyU2l6ZV06IF9FdWlUaGVtZVNpemU7XG59ID0ge1xuICBzOiAnbScsXG4gIG06ICdiYXNlJyxcbiAgbDogJ2wnLFxuICB4bDogJ3hsJyxcbiAgeHhsOiAneHhsJyxcbn07XG5cbmNvbnN0IHNwaW5uZXJDb2xvcnMgPSAobWFpbjogc3RyaW5nLCBoaWdobGlnaHQ6IHN0cmluZykgPT4ge1xuICByZXR1cm4gYCR7aGlnaGxpZ2h0fSAke21haW59ICR7bWFpbn0gJHttYWlufWA7XG59O1xuXG5leHBvcnQgY29uc3QgZXVpTG9hZGluZ1NwaW5uZXJTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4ge1xuICByZXR1cm4ge1xuICAgIGV1aUxvYWRpbmdTcGlubmVyOiBjc3NgXG4gICAgICBmbGV4LXNocmluazogMDsgLy8gRW5zdXJlcyBpdCBuZXZlciBzY2FsZXMgZG93biBiZWxvdyBpdHMgaW50ZW5kZWQgc2l6ZVxuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgICAgYm9yZGVyOiAke2V1aVRoZW1lLmJvcmRlci50aGlja307XG4gICAgICBib3JkZXItY29sb3I6ICR7c3Bpbm5lckNvbG9ycyhcbiAgICAgICAgZXVpVGhlbWUuYm9yZGVyLmNvbG9yLFxuICAgICAgICBldWlUaGVtZS5jb2xvcnMucHJpbWFyeVxuICAgICAgKX07XG5cbiAgICAgICR7ZXVpQ2FuQW5pbWF0ZX0ge1xuICAgICAgICBhbmltYXRpb246ICR7X2xvYWRpbmdTcGlubmVyfSAwLjZzIGluZmluaXRlIGxpbmVhcjtcbiAgICAgIH1cbiAgICBgLFxuXG4gICAgLy8gU2l6ZXNcbiAgICBzOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5zXX07XG4gICAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMuc119O1xuICAgICAgYm9yZGVyLXdpZHRoOiBjYWxjKCR7ZXVpVGhlbWUuYm9yZGVyLndpZHRoLnRoaW59ICogMS41KTtcbiAgICBgLFxuICAgIG06IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLm1dfTtcbiAgICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5tXX07XG4gICAgICBib3JkZXItd2lkdGg6IGNhbGMoJHtldWlUaGVtZS5ib3JkZXIud2lkdGgudGhpbn0gKiAxLjUpO1xuICAgIGAsXG4gICAgbDogY3NzYFxuICAgICAgd2lkdGg6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMubF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLmxdfTtcbiAgICBgLFxuICAgIHhsOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy54bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnhsXX07XG4gICAgYCxcbiAgICB4eGw6IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgIGAsXG4gIH07XG59O1xuIl19 */
</style>,
<style
data-emotion="css"
data-s=""
>
@-webkit-keyframes animation-c24ia8{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(359deg);-moz-transform:rotate(359deg);-ms-transform:rotate(359deg);transform:rotate(359deg);}}
</style>,
<style
data-emotion="css"
data-s=""
>
@keyframes animation-c24ia8{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(359deg);-moz-transform:rotate(359deg);-ms-transform:rotate(359deg);transform:rotate(359deg);}}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9jb21wb25lbnRzL2xvYWRpbmcvbG9hZGluZ19zcGlubmVyLnN0eWxlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE0RFUiLCJmaWxlIjoiLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvbG9hZGluZy9sb2FkaW5nX3NwaW5uZXIuc3R5bGVzLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLypcbiAqIENvcHlyaWdodCBFbGFzdGljc2VhcmNoIEIuVi4gYW5kL29yIGxpY2Vuc2VkIHRvIEVsYXN0aWNzZWFyY2ggQi5WLiB1bmRlciBvbmVcbiAqIG9yIG1vcmUgY29udHJpYnV0b3IgbGljZW5zZSBhZ3JlZW1lbnRzLiBMaWNlbnNlZCB1bmRlciB0aGUgRWxhc3RpYyBMaWNlbnNlXG4gKiAyLjAgYW5kIHRoZSBTZXJ2ZXIgU2lkZSBQdWJsaWMgTGljZW5zZSwgdiAxOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdFxuICogaW4gY29tcGxpYW5jZSB3aXRoLCBhdCB5b3VyIGVsZWN0aW9uLCB0aGUgRWxhc3RpYyBMaWNlbnNlIDIuMCBvciB0aGUgU2VydmVyXG4gKiBTaWRlIFB1YmxpYyBMaWNlbnNlLCB2IDEuXG4gKi9cblxuaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9yZWFjdCc7XG5pbXBvcnQgeyBfRXVpVGhlbWVTaXplLCBldWlDYW5BbmltYXRlIH0gZnJvbSAnLi4vLi4vZ2xvYmFsX3N0eWxpbmcnO1xuaW1wb3J0IHsgVXNlRXVpVGhlbWUgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcyc7XG5pbXBvcnQgeyBFdWlMb2FkaW5nU3Bpbm5lclNpemUgfSBmcm9tICcuL2xvYWRpbmdfc3Bpbm5lcic7XG5cbmNvbnN0IF9sb2FkaW5nU3Bpbm5lciA9IGtleWZyYW1lc2BcbiAgZnJvbSB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTtcbiAgfVxuYDtcblxuY29uc3Qgc3Bpbm5lclNpemVzOiB7XG4gIFtzaXplIGluIEV1aUxvYWRpbmdTcGlubmVyU2l6ZV06IF9FdWlUaGVtZVNpemU7XG59ID0ge1xuICBzOiAnbScsXG4gIG06ICdiYXNlJyxcbiAgbDogJ2wnLFxuICB4bDogJ3hsJyxcbiAgeHhsOiAneHhsJyxcbn07XG5cbmNvbnN0IHNwaW5uZXJDb2xvcnMgPSAobWFpbjogc3RyaW5nLCBoaWdobGlnaHQ6IHN0cmluZykgPT4ge1xuICByZXR1cm4gYCR7aGlnaGxpZ2h0fSAke21haW59ICR7bWFpbn0gJHttYWlufWA7XG59O1xuXG5leHBvcnQgY29uc3QgZXVpTG9hZGluZ1NwaW5uZXJTdHlsZXMgPSAoeyBldWlUaGVtZSB9OiBVc2VFdWlUaGVtZSkgPT4ge1xuICByZXR1cm4ge1xuICAgIGV1aUxvYWRpbmdTcGlubmVyOiBjc3NgXG4gICAgICBmbGV4LXNocmluazogMDsgLy8gRW5zdXJlcyBpdCBuZXZlciBzY2FsZXMgZG93biBiZWxvdyBpdHMgaW50ZW5kZWQgc2l6ZVxuICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgICAgYm9yZGVyOiAke2V1aVRoZW1lLmJvcmRlci50aGlja307XG4gICAgICBib3JkZXItY29sb3I6ICR7c3Bpbm5lckNvbG9ycyhcbiAgICAgICAgZXVpVGhlbWUuYm9yZGVyLmNvbG9yLFxuICAgICAgICBldWlUaGVtZS5jb2xvcnMucHJpbWFyeVxuICAgICAgKX07XG5cbiAgICAgICR7ZXVpQ2FuQW5pbWF0ZX0ge1xuICAgICAgICBhbmltYXRpb246ICR7X2xvYWRpbmdTcGlubmVyfSAwLjZzIGluZmluaXRlIGxpbmVhcjtcbiAgICAgIH1cbiAgICBgLFxuXG4gICAgLy8gU2l6ZXNcbiAgICBzOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5zXX07XG4gICAgICBoZWlnaHQ6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMuc119O1xuICAgICAgYm9yZGVyLXdpZHRoOiBjYWxjKCR7ZXVpVGhlbWUuYm9yZGVyLndpZHRoLnRoaW59ICogMS41KTtcbiAgICBgLFxuICAgIG06IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLm1dfTtcbiAgICAgIGhlaWdodDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy5tXX07XG4gICAgICBib3JkZXItd2lkdGg6IGNhbGMoJHtldWlUaGVtZS5ib3JkZXIud2lkdGgudGhpbn0gKiAxLjUpO1xuICAgIGAsXG4gICAgbDogY3NzYFxuICAgICAgd2lkdGg6ICR7ZXVpVGhlbWUuc2l6ZVtzcGlubmVyU2l6ZXMubF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLmxdfTtcbiAgICBgLFxuICAgIHhsOiBjc3NgXG4gICAgICB3aWR0aDogJHtldWlUaGVtZS5zaXplW3NwaW5uZXJTaXplcy54bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnhsXX07XG4gICAgYCxcbiAgICB4eGw6IGNzc2BcbiAgICAgIHdpZHRoOiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgICAgaGVpZ2h0OiAke2V1aVRoZW1lLnNpemVbc3Bpbm5lclNpemVzLnh4bF19O1xuICAgIGAsXG4gIH07XG59O1xuIl19 */",
"name": "155ohbx-euiLoadingSpinner-m",
"next": Object {
"name": "animation-c24ia8",
"next": undefined,
"styles": "@keyframes animation-c24ia8{
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}",
},
"styles": "flex-shrink:0;display:inline-block;border-radius:50%;border:2px solid #D3DAE6;border-color:#07C #D3DAE6 #D3DAE6 #D3DAE6;@media screen and (prefers-reduced-motion: no-preference){animation:animation-c24ia8 0.6s infinite linear;};label:euiLoadingSpinner;;;width:16px;height:16px;border-width:calc(1px * 1.5);;label:m;;;",
"toString": [Function],
}
}
/>
<span
aria-label="Loading"
className="euiLoadingSpinner css-155ohbx-euiLoadingSpinner-m"
role="progressbar"
/>
</span>
</EuiLoadingSpinner>
</div>
</Fallback>
</Suspense>
</EuiErrorBoundary>
</ForwardRef>
</AnalyticsNoDataPage>
`;

View file

@ -7,21 +7,31 @@
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { KibanaNoDataPage } from '@kbn/shared-ux-components';
import { KibanaNoDataPage } from '@kbn/shared-ux-page-kibana-no-data';
import { AnalyticsNoDataPage } from './analytics_no_data_page.component';
import { AnalyticsNoDataPageProvider } from './services';
import { getMockServices } from './mocks';
describe('AnalyticsNoDataPageComponent', () => {
const services = getMockServices();
const onDataViewCreated = jest.fn();
it('renders correctly', () => {
it('renders correctly', async () => {
const component = mountWithIntl(
// Include context so composed components will have access to their services.
<AnalyticsNoDataPageProvider {...services}>
<AnalyticsNoDataPage
onDataViewCreated={onDataViewCreated}
kibanaGuideDocLink={'http://www.test.com'}
/>
</AnalyticsNoDataPageProvider>
);
expect(component).toMatchSnapshot();
await act(() => new Promise(setImmediate));
expect(component.find(KibanaNoDataPage).length).toBe(1);

View file

@ -7,13 +7,15 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { KibanaNoDataPage } from '@kbn/shared-ux-components';
import { KibanaNoDataPage } from '@kbn/shared-ux-page-kibana-no-data';
/**
* Props for the pure component.
*/
export interface Props {
/** A link to documentation. */
kibanaGuideDocLink: string;
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
}

View file

@ -8,14 +8,15 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { servicesFactory, DataServiceFactoryConfig } from '@kbn/shared-ux-storybook';
import { AnalyticsNoDataPage as Component } from './analytics_no_data_page';
import { AnalyticsNoDataPageProvider, Services } from './services';
import { AnalyticsNoDataPageProvider } from './services';
import mdx from '../README.mdx';
import { Params, getStoryArgTypes, getStoryServices } from './mocks';
export default {
title: 'Analytics No Data Page',
title: 'No Data/Analytics',
description: 'An Analytics-specific version of KibanaNoDataPage.',
parameters: {
docs: {
@ -24,22 +25,27 @@ export default {
},
};
type Params = Pick<DataServiceFactoryConfig, 'hasESData' | 'hasUserDataView'>;
export const AnalyticsNoDataPage = (params: Params) => {
// Workaround to leverage the services package.
const { application, data, docLinks, editors, http, permissions, platform } =
servicesFactory(params);
return (
<AnalyticsNoDataPageProvider {...getStoryServices(params)}>
<Component onDataViewCreated={action('onDataViewCreated')} />
</AnalyticsNoDataPageProvider>
);
};
const services: Services = {
...application,
...data,
...docLinks,
...editors,
...http,
...permissions,
...platform,
kibanaGuideDocLink: 'Kibana guide',
AnalyticsNoDataPage.argTypes = {
...getStoryArgTypes(),
};
export const LoadingState = (params: Params) => {
// Simulate loading with a Promise that doesn't resolve.
const dataCheck = () => new Promise<boolean>((_reject, _resolve) => {});
const services = {
...getStoryServices(params),
hasESData: dataCheck,
hasUserDataView: dataCheck,
hasDataView: dataCheck,
};
return (
@ -48,14 +54,3 @@ export const AnalyticsNoDataPage = (params: Params) => {
</AnalyticsNoDataPageProvider>
);
};
AnalyticsNoDataPage.argTypes = {
hasESData: {
control: 'boolean',
defaultValue: false,
},
hasUserDataView: {
control: 'boolean',
defaultValue: false,
},
};

View file

@ -7,30 +7,18 @@
*/
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { mockServicesFactory } from '@kbn/shared-ux-services';
import { Services, AnalyticsNoDataPageProvider } from './services';
import { AnalyticsNoDataPageProvider } from './services';
import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component';
import { AnalyticsNoDataPage } from './analytics_no_data_page';
import { getMockServices } from './mocks';
describe('AnalyticsNoDataPage', () => {
const onDataViewCreated = jest.fn();
// Workaround to leverage the services package.
const { application, data, docLinks, editors, http, permissions, platform } =
mockServicesFactory();
const services: Services = {
...application,
...data,
...docLinks,
...editors,
...http,
...permissions,
...platform,
kibanaGuideDocLink: 'Kibana guide',
};
const services = getMockServices();
afterAll(() => {
jest.resetAllMocks();
@ -43,6 +31,8 @@ describe('AnalyticsNoDataPage', () => {
</AnalyticsNoDataPageProvider>
);
await act(() => new Promise(setImmediate));
expect(component.find(Component).length).toBe(1);
expect(component.find(Component).props().kibanaGuideDocLink).toBe(services.kibanaGuideDocLink);
expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated);

View file

@ -6,8 +6,6 @@
* Side Public License, v 1.
*/
import React from 'react';
import { LegacyServicesProvider, getLegacyServices } from './legacy_services';
import { useServices } from './services';
import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component';
@ -15,25 +13,24 @@ import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.compo
* Props for the `AnalyticsNoDataPage` component.
*/
export interface AnalyticsNoDataPageProps {
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
}
/**
* An entire page that can be displayed when Kibana "has no data", specifically for Analytics. Uses
* services from a provider to provide props to a pure component.
* services from a Provider to supply props to a pure component.
*/
export const AnalyticsNoDataPage = ({ onDataViewCreated }: AnalyticsNoDataPageProps) => {
const services = useServices();
const { kibanaGuideDocLink } = services;
return (
<LegacyServicesProvider {...getLegacyServices(services)}>
<Component
{...{
onDataViewCreated,
kibanaGuideDocLink,
}}
/>
</LegacyServicesProvider>
);
};

View file

@ -8,3 +8,10 @@
export { AnalyticsNoDataPageProvider, AnalyticsNoDataPageKibanaProvider } from './services';
export { AnalyticsNoDataPage } from './analytics_no_data_page';
export { AnalyticsNoDataPage as AnalyticsNoDataPageComponent } from './analytics_no_data_page.component';
export {
getMockServices as getAnalyticsNoDataPageMockServices,
getStoryArgTypes as getAnalyticsNoDataPageStoryArgTypes,
getStoryServices as getAnalyticsNoDataPageStoryServices,
} from './mocks';

View file

@ -0,0 +1,49 @@
/*
* 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 {
getKibanaNoDataPageStoryArgTypes,
getKibanaNoDataPageStoryMock,
getKibanaNoDataPageMockServices,
} from '@kbn/shared-ux-page-kibana-no-data';
import { AnalyticsNoDataPageServices } from './services';
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
export const getStoryServices = (params: Params) => {
const services: AnalyticsNoDataPageServices = {
...getKibanaNoDataPageStoryMock({ ...params, solution: 'Analytics', logo: 'logoKibana' }),
kibanaGuideDocLink: 'Kibana guide',
};
return services;
};
export const getStoryArgTypes = () => {
// While `solution` and `logo` are props on `KibanaNoDataPage`, they are set by
// `AnalyticsNoDataPage` internally. So calls to `getStoryArgTypes` for `AnalyticsNoDataPage`
// need to remove them for any of its stories, (as they'll have no effect).
const { solution, logo, ...args } = getKibanaNoDataPageStoryArgTypes();
return {
kibanaGuideDocLink: {
control: 'text',
defaultValue: 'Kibana guide',
},
...args,
};
};
export const getMockServices = () => {
const services: AnalyticsNoDataPageServices = {
...getKibanaNoDataPageMockServices(),
kibanaGuideDocLink: 'Kibana guide',
};
return services;
};

View file

@ -7,114 +7,60 @@
*/
import React, { FC, useContext } from 'react';
import { Observable } from 'rxjs';
import {
KibanaNoDataPageServices,
KibanaNoDataPageKibanaDependencies,
KibanaNoDataPageKibanaProvider,
KibanaNoDataPageProvider,
} from '@kbn/shared-ux-page-kibana-no-data';
/**
* TODO: `DataView` is a class exported by `src/plugins/data_views/public`. Since this service
* is contained in this package-- and packages can only depend on other packages and never on
* plugins-- we have to set this to `unknown`. If and when `DataView` is exported from a
* stateless package, we can remove this.
*
* @see: https://github.com/elastic/kibana/issues/127695
* A list of services that are consumed by this component.
*/
type DataView = unknown;
/**
* A subset of the `DataViewEditorOptions` interface relevant to this component.
*
* @see: src/plugins/data_view_editor/public/types.ts
*/
interface DataViewEditorOptions {
/** Handler to be invoked when the Data View Editor completes a save operation. */
onSave: (dataView: DataView) => void;
/** If set to false, will skip empty prompt in data view editor. */
showEmptyPrompt?: boolean;
}
/**
* A list of Services that are consumed by this component.
*
* This list is temporary, a stopgap as we migrate to a package-based architecture, where
* services are not collected in a single package. In order to make the transition, this
* interface is intentionally "flat".
*
* Expect this list to dwindle to zero as `@kbn/shared-ux-components` are migrated to their
* own packages, (and `@kbn/shared-ux-services` is removed).
*/
export interface Services {
addBasePath: (url: string) => string;
canAccessFleet: boolean;
canCreateNewDataView: boolean;
currentAppId$: Observable<string | undefined>;
dataViewsDocLink: string;
hasDataView: () => Promise<boolean>;
hasESData: () => Promise<boolean>;
hasUserDataView: () => Promise<boolean>;
interface Services {
kibanaGuideDocLink: string;
navigateToUrl: (url: string) => Promise<void>;
openDataViewEditor: (options: DataViewEditorOptions) => () => void;
setIsFullscreen: (isFullscreen: boolean) => void;
}
const AnalyticsNoDataPageContext = React.createContext<Services | null>(null);
const Context = React.createContext<Services | null>(null);
/**
* A Context Provider that provides services to the component.
* Services that are consumed by this component and its dependencies.
*/
export const AnalyticsNoDataPageProvider: FC<Services> = ({ children, ...services }) => {
export type AnalyticsNoDataPageServices = Services & KibanaNoDataPageServices;
/**
* A Context Provider that provides services to the component and its dependencies.
*/
export const AnalyticsNoDataPageProvider: FC<AnalyticsNoDataPageServices> = ({
children,
...services
}) => {
const { kibanaGuideDocLink } = services;
return (
<AnalyticsNoDataPageContext.Provider value={services}>
{children}
</AnalyticsNoDataPageContext.Provider>
<Context.Provider value={{ kibanaGuideDocLink }}>
<KibanaNoDataPageProvider {...services}>{children}</KibanaNoDataPageProvider>
</Context.Provider>
);
};
/**
* An interface containing a collection of Kibana plugins and services required to
* render this component and its dependencies.
*/
export interface AnalyticsNoDataPageKibanaDependencies {
interface KibanaDependencies {
coreStart: {
application: {
capabilities: {
navLinks: Record<string, boolean>;
};
currentAppId$: Observable<string | undefined>;
navigateToUrl: (url: string) => Promise<void>;
};
chrome: {
setIsVisible: (isVisible: boolean) => void;
};
docLinks: {
links: {
indexPatterns: {
introduction: string;
};
kibana: {
guide: string;
};
};
};
http: {
basePath: {
prepend: (url: string) => string;
};
};
};
dataViews: {
hasData: {
hasDataView: () => Promise<boolean>;
hasESData: () => Promise<boolean>;
hasUserDataView: () => Promise<boolean>;
};
};
dataViewEditor: {
openEditor: (options: DataViewEditorOptions) => () => void;
userPermissions: {
editDataView: () => boolean;
};
};
}
/**
* An interface containing a collection of Kibana plugins and services required to
* render this component as well as its dependencies.
*/
export type AnalyticsNoDataPageKibanaDependencies = KibanaDependencies &
KibanaNoDataPageKibanaDependencies;
/**
* Kibana-specific Provider that maps dependencies to services.
@ -123,26 +69,14 @@ export const AnalyticsNoDataPageKibanaProvider: FC<AnalyticsNoDataPageKibanaDepe
children,
...dependencies
}) => {
const { coreStart, dataViewEditor, dataViews } = dependencies;
const value: Services = {
addBasePath: coreStart.http.basePath.prepend,
canAccessFleet: coreStart.application.capabilities.navLinks.integrations,
canCreateNewDataView: dataViewEditor.userPermissions.editDataView(),
currentAppId$: coreStart.application.currentAppId$,
dataViewsDocLink: coreStart.docLinks.links.indexPatterns?.introduction,
hasDataView: dataViews.hasData.hasDataView,
hasESData: dataViews.hasData.hasESData,
hasUserDataView: dataViews.hasData.hasUserDataView,
kibanaGuideDocLink: coreStart.docLinks.links.kibana.guide,
navigateToUrl: coreStart.application.navigateToUrl,
openDataViewEditor: dataViewEditor.openEditor,
setIsFullscreen: (isVisible: boolean) => coreStart.chrome.setIsVisible(isVisible),
kibanaGuideDocLink: dependencies.coreStart.docLinks.links.kibana.guide,
};
return (
<AnalyticsNoDataPageContext.Provider value={value}>
{children}
</AnalyticsNoDataPageContext.Provider>
<Context.Provider {...{ value }}>
<KibanaNoDataPageKibanaProvider {...dependencies}>{children}</KibanaNoDataPageKibanaProvider>
</Context.Provider>
);
};
@ -150,7 +84,7 @@ export const AnalyticsNoDataPageKibanaProvider: FC<AnalyticsNoDataPageKibanaDepe
* React hook for accessing pre-wired services.
*/
export function useServices() {
const context = useContext(AnalyticsNoDataPageContext);
const context = useContext(Context);
if (!context) {
throw new Error(

View file

@ -0,0 +1,148 @@
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 = "kibana_no_data"
PKG_REQUIRE_NAME = "@kbn/shared-ux-page-kibana-no-data"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.mdx",
],
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//@emotion/css",
"@npm//@emotion/react",
"@npm//@storybook/addon-actions",
"@npm//react",
"//packages/kbn-i18n",
"//packages/kbn-shared-ux-components",
"//packages/kbn-shared-ux-services",
"//packages/kbn-shared-ux-storybook",
"//packages/shared-ux/prompt/no_data_views",
]
# 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//@emotion/css",
"@npm//@emotion/react",
"@npm//@storybook/addon-actions",
"@npm//@types/jest",
"@npm//@types/node",
"@npm//@types/react",
"//packages/kbn-ambient-ui-types",
"//packages/kbn-i18n:npm_module_types",
"//packages/kbn-shared-ux-components:npm_module_types",
"//packages/kbn-shared-ux-services:npm_module_types",
"//packages/kbn-shared-ux-storybook:npm_module_types",
"//packages/shared-ux/prompt/no_data_views: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/Page/KibanaNoDataPage
slug: /shared-ux/page/kibana-no-data
title: Kibana No Data Page
summary: A page to be displayed when there is no data in Elasticsearch, or no data views.
tags: ['shared-ux', 'component']
date: 2022-04-20
---
Many plugins display a "no data" page, either when there is no data in Elasticsearch, or there haven't been any data views created yet. This component is meant
to be used in those scenarios. It displays an appropriate message to the user and facilitate addition of integrations and creation of data views.
The `KibanaNoDataPage` connected component uses:
- `hasUserDataView` and `hasData` API from the `HasData` service in the `data_views` plugin to check for existence of data an data views.
- `onDataViewCreated` callback once a data view has been created.
- (noDataConfig)[https://github.com/elastic/kibana/blob/main/packages/kbn-shared-ux-components/src/page_template/no_data_page/types.ts] as configuration for the page in case of no data.
## API
| Export | Description |
|---|---|
| `KibanaNoDataPageProvider` | Provides contextual services to `KibanaNoDataPage`. |
| `KibanaNoDataPageKibanaProvider` | Maps Kibana dependencies to provide contextual services to `KibanaNoDataPage`. |
| `KibanaNoDataPage` | Uses a `Provider` to access contextual services and render the component. |
## EUI Promotion Status
This component is not currently considered for promotion to EUI.

View file

@ -6,4 +6,8 @@
* Side Public License, v 1.
*/
export { KibanaNoDataPage } from './kibana_no_data_page';
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/page/kibana_no_data'],
};

View file

@ -0,0 +1,8 @@
{
"name": "@kbn/shared-ux-page-kibana-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,18 @@
/*
* 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 type { KibanaNoDataPageServices, KibanaNoDataPageKibanaDependencies } from './services';
export { KibanaNoDataPage } from './kibana_no_data_page';
export { KibanaNoDataPageKibanaProvider, KibanaNoDataPageProvider } from './services';
export {
getStoryArgTypes as getKibanaNoDataPageStoryArgTypes,
getStoryServices as getKibanaNoDataPageStoryMock,
getMockServices as getKibanaNoDataPageMockServices,
} from './mocks';

View file

@ -0,0 +1,72 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { KibanaNoDataPage as Component } from './kibana_no_data_page';
import { StoryParams, getStoryServices, getStoryArgTypes } from './mocks';
import { KibanaNoDataPageProvider } from './services';
import mdx from '../README.mdx';
export default {
title: 'No Data/Kibana',
description: 'A component to display when there is no data available',
parameters: {
docs: {
page: mdx,
},
},
};
const noDataConfig = {
solution: 'Analytics',
logo: 'logoKibana',
action: {
elasticAgent: {
title: 'Add Integrations',
},
},
docsLink: 'http://docs.elastic.dev',
};
export const KibanaNoDataPage = (params: StoryParams) => {
const { solution, logo } = params;
return (
<KibanaNoDataPageProvider {...getStoryServices(params)}>
<Component
onDataViewCreated={action('onDataViewCreated')}
noDataConfig={{ ...noDataConfig, solution, logo }}
/>
</KibanaNoDataPageProvider>
);
};
KibanaNoDataPage.argTypes = {
...getStoryArgTypes(),
};
export const LoadingState = (params: StoryParams) => {
// Simulate loading with a Promise that doesn't resolve.
const dataCheck = () => new Promise<boolean>((resolve, reject) => {});
const services = {
...getStoryServices(params),
hasESData: dataCheck,
hasUserDataView: dataCheck,
hasDataView: dataCheck,
};
return (
<KibanaNoDataPageProvider {...services}>
<Component onDataViewCreated={action('onDataViewCreated')} noDataConfig={noDataConfig} />
</KibanaNoDataPageProvider>
);
};

View file

@ -11,11 +11,12 @@ import { act } from 'react-dom/test-utils';
import { EuiLoadingElastic } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { SharedUxServicesProvider, mockServicesFactory } from '@kbn/shared-ux-services';
import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views';
import { NoDataConfigPage } from '@kbn/shared-ux-components';
import { KibanaNoDataPage } from './kibana_no_data_page';
import { NoDataConfigPage } from '../page_template';
import { KibanaNoDataPageProvider } from './services';
import { getMockServices } from './mocks';
describe('Kibana No Data Page', () => {
const noDataConfig = {
@ -29,6 +30,7 @@ describe('Kibana No Data Page', () => {
},
docsLink: 'http://www.docs.com',
};
const onDataViewCreated = jest.fn();
const config = {
hasESData: false,
@ -41,11 +43,11 @@ describe('Kibana No Data Page', () => {
});
test('renders NoDataConfigPage', async () => {
const services = mockServicesFactory({ config: { ...config, hasESData: false } });
const services = getMockServices({ config: { ...config, hasESData: false } });
const component = mountWithIntl(
<SharedUxServicesProvider {...services}>
<KibanaNoDataPage noDataConfig={noDataConfig} onDataViewCreated={onDataViewCreated} />
</SharedUxServicesProvider>
<KibanaNoDataPageProvider {...services}>
<KibanaNoDataPage {...{ noDataConfig, onDataViewCreated }} />
</KibanaNoDataPageProvider>
);
await act(() => new Promise(setImmediate));
@ -56,11 +58,11 @@ describe('Kibana No Data Page', () => {
});
test('renders NoDataViews', async () => {
const services = mockServicesFactory({ config: { ...config, hasESData: true } });
const services = getMockServices({ config: { ...config, hasESData: true } });
const component = mountWithIntl(
<SharedUxServicesProvider {...services}>
<KibanaNoDataPageProvider {...services}>
<KibanaNoDataPage noDataConfig={noDataConfig} onDataViewCreated={onDataViewCreated} />
</SharedUxServicesProvider>
</KibanaNoDataPageProvider>
);
await act(() => new Promise(setImmediate));
@ -71,23 +73,24 @@ describe('Kibana No Data Page', () => {
});
test('renders loading indicator', async () => {
const dataCheck = () => new Promise<boolean>((resolve, reject) => {});
// Simulate loading with a Promise that doesn't resolve.
const dataCheck = () => new Promise<boolean>(() => {});
const services = {
...mockServicesFactory(),
...getMockServices(),
data: {
hasESData: dataCheck,
hasUserDataView: dataCheck,
hasDataView: dataCheck,
},
};
const component = mountWithIntl(
<SharedUxServicesProvider {...services}>
<KibanaNoDataPageProvider {...services}>
<KibanaNoDataPage noDataConfig={noDataConfig} onDataViewCreated={onDataViewCreated} />
</SharedUxServicesProvider>
</KibanaNoDataPageProvider>
);
await act(() => new Promise(setImmediate));
component.update();
expect(component.find(EuiLoadingElastic).length).toBe(1);
expect(component.find(NoDataViewsPrompt).length).toBe(0);

View file

@ -6,27 +6,30 @@
* Side Public License, v 1.
*/
import React, { useEffect, useState } from 'react';
import { useData, useDocLinks, useEditors, usePermissions } from '@kbn/shared-ux-services';
import {
NoDataViewsPrompt,
NoDataViewsPromptProvider,
NoDataViewsPromptServices,
} from '@kbn/shared-ux-prompt-no-data-views';
import { EuiLoadingElastic } from '@elastic/eui';
import { NoDataConfigPage, NoDataPageProps } from '../page_template';
import { NoDataConfigPage, NoDataPageProps } from '@kbn/shared-ux-components';
import { NoDataViewsPrompt } from '@kbn/shared-ux-prompt-no-data-views';
import { useServices } from './services';
/**
* Props for `KibanaNoDataPage`.
*/
export interface Props {
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
/** `NoDataPage` configuration; see `NoDataPageProps`. */
noDataConfig: NoDataPageProps;
}
/**
* A page to display when Kibana has no data, prompting a person to add integrations or create a new data view.
*/
export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) => {
// These hooks are temporary, until this component is moved to a package.
const { canCreateNewDataView } = usePermissions();
const { dataViewsDocLink } = useDocLinks();
const { openDataViewEditor } = useEditors();
const services = useServices();
const { hasESData, hasUserDataView } = services;
const { hasESData, hasUserDataView } = useData();
const [isLoading, setIsLoading] = useState(true);
const [dataExists, setDataExists] = useState(false);
const [hasUserDataViews, setHasUserDataViews] = useState(false);
@ -48,26 +51,8 @@ export const KibanaNoDataPage = ({ onDataViewCreated, noDataConfig }: Props) =>
return <EuiLoadingElastic css={{ margin: 'auto' }} size="xxl" />;
}
/*
TODO: clintandrewhall - the use and population of `NoDataViewPromptProvider` here is temporary,
until `KibanaNoDataPage` is moved to a package of its own.
Once `KibanaNoDataPage` is moved to a package, `NoDataViewsPromptProvider` will be *combined*
with `KibanaNoDataPageProvider`, creating a single Provider that manages contextual dependencies
throughout the React tree from the top-level of composition and consumption.
*/
if (!hasUserDataViews && dataExists) {
const services: NoDataViewsPromptServices = {
canCreateNewDataView,
dataViewsDocLink,
openDataViewEditor,
};
return (
<NoDataViewsPromptProvider {...services}>
<NoDataViewsPrompt onDataViewCreated={onDataViewCreated} />
</NoDataViewsPromptProvider>
);
return <NoDataViewsPrompt onDataViewCreated={onDataViewCreated} />;
}
if (!dataExists) {

View file

@ -10,7 +10,7 @@ export { SharedUxServicesProvider as LegacyServicesProvider } from '@kbn/shared-
export type { SharedUxServices as LegacyServices } from '@kbn/shared-ux-services';
import { SharedUxServices as LegacyServices } from '@kbn/shared-ux-services';
import { Services } from './services';
import { KibanaNoDataPageServices } from './services';
/**
* This list is temporary, a stop-gap as we migrate to a package-based architecture, where
@ -20,7 +20,7 @@ import { Services } from './services';
* Expect this list to dwindle to zero as `@kbn/shared-ux-components` are migrated to their
* own packages, (and `@kbn/shared-ux-services` is removed).
*/
export const getLegacyServices = (services: Services): LegacyServices => ({
export const getLegacyServices = (services: KibanaNoDataPageServices): LegacyServices => ({
application: {
currentAppId$: services.currentAppId$,
navigateToUrl: services.navigateToUrl,

View file

@ -0,0 +1,104 @@
/*
* 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 { servicesFactory } from '@kbn/shared-ux-storybook';
import { mockServicesFactory, MockServicesFactoryParams } from '@kbn/shared-ux-services';
import {
getNoDataViewsPromptStoryArgTypes,
getNoDataViewsPromptStorybookServices,
getNoDataViewsPromptMockServices,
} from '@kbn/shared-ux-prompt-no-data-views';
import { KibanaNoDataPageServices } from './services';
// TODO: clintandrewhall - this looks (and is) a bit complicated because the No Data View
// dependency has not been converted to its own package yet. As with `AnalyticsNoDataPage`,
// this file will be significantly simplified when that happens.
/**
* Parameters drawn from the Storybook arguments collection that customize a component story.
*/
export type StoryParams = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
/**
* Returns Storybook-compatible service abstractions for the `KibanaNoDataPage` Provider.
*/
export const getStoryServices = (params: StoryParams) => {
const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } =
getNoDataViewsPromptStorybookServices(params);
// Workaround to leverage the services package.
const { application, data, docLinks, editors, http, permissions, platform } =
servicesFactory(params);
const services: KibanaNoDataPageServices = {
...application,
...data,
...docLinks,
...editors,
...http,
...permissions,
...platform,
canCreateNewDataView,
dataViewsDocLink,
openDataViewEditor,
};
return services;
};
/**
* Returns the Storybook arguments for `KibanaNoDataPage`, for its stories for and for
* consuming component stories.
*/
export const getStoryArgTypes = () => ({
solution: {
control: 'text',
defaultValue: 'Observability',
},
logo: {
control: { type: 'radio' },
options: ['logoElastic', 'logoKibana', 'logoCloud', undefined],
defaultValue: undefined,
},
hasESData: {
control: 'boolean',
defaultValue: false,
},
hasUserDataView: {
control: 'boolean',
defaultValue: false,
},
...getNoDataViewsPromptStoryArgTypes(),
});
/**
* Returns the Jest-compatible service abstractions for the `KibanaNoDataPage` Provider.
*/
export const getMockServices = (params?: MockServicesFactoryParams) => {
const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } =
getNoDataViewsPromptMockServices();
const { application, data, docLinks, editors, http, permissions, platform } =
mockServicesFactory(params);
const services: KibanaNoDataPageServices = {
...application,
...data,
...docLinks,
...editors,
...http,
...permissions,
...platform,
canCreateNewDataView,
dataViewsDocLink,
openDataViewEditor,
};
return services;
};

View file

@ -0,0 +1,181 @@
/*
* 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 { Observable } from 'rxjs';
import {
NoDataViewsPromptProvider,
NoDataViewsPromptKibanaProvider,
} from '@kbn/shared-ux-prompt-no-data-views';
import { LegacyServicesProvider, getLegacyServices } from './legacy_services';
/**
* TODO: `DataView` is a class exported by `src/plugins/data_views/public`. Since this service
* is contained in this package-- and packages can only depend on other packages and never on
* plugins-- we have to set this to `unknown`. If and when `DataView` is exported from a
* stateless package, we can remove this.
*
* @see: https://github.com/elastic/kibana/issues/127695
*/
type DataView = unknown;
/**
* A subset of the `DataViewEditorOptions` interface relevant to this component.
*
* @see: src/plugins/data_view_editor/public/types.ts
*/
interface DataViewEditorOptions {
/** Handler to be invoked when the Data View Editor completes a save operation. */
onSave: (dataView: DataView) => void;
/** If set to false, will skip empty prompt in data view editor. */
showEmptyPrompt?: boolean;
}
/**
* A list of Services that are consumed by this component.
*
* This list is temporary, a stopgap as we migrate to a package-based architecture, where
* services are not collected in a single package. In order to make the transition, this
* interface is intentionally "flat".
*
* Expect this list to dwindle to zero as `@kbn/shared-ux-components` are migrated to their
* own packages, (and `@kbn/shared-ux-services` is removed).
*/
export interface KibanaNoDataPageServices {
/** True if the cluster contains data, false otherwise. */
hasESData: () => Promise<boolean>;
/** True if Kibana instance contains user-created data view, false otherwise. */
hasUserDataView: () => Promise<boolean>;
// Provided to Legacy Services, not relevant to this component. Will be removed.
/** Append the server base path to a relative URL. */
addBasePath: (url: string) => string;
/** True if the user has permission to access Fleet, false otherwise. */
canAccessFleet: boolean;
/** True if the user has permission to create a new Data View, false otherwise. */
canCreateNewDataView: boolean;
/** Observable storing the active, current application ID. */
currentAppId$: Observable<string | undefined>;
/** A link to information about Data Views in Kibana */
dataViewsDocLink: string;
/** True if Kibana instance contains any data view, including system-created ones. */
hasDataView: () => Promise<boolean>;
/** Use Kibana to navigate async to a different URL. */
navigateToUrl: (url: string) => Promise<void>;
/** A method to open the Data View Editor flow. */
openDataViewEditor: (options: DataViewEditorOptions) => () => void;
/** Set the Kibana chrome and browser to full screen mode. */
setIsFullscreen: (isFullscreen: boolean) => void;
}
const KibanaNoDataPageContext = React.createContext<KibanaNoDataPageServices | null>(null);
/**
* A Context Provider that provides services to the component.
*/
export const KibanaNoDataPageProvider: FC<KibanaNoDataPageServices> = ({
children,
...services
}) => (
<KibanaNoDataPageContext.Provider value={services}>
<NoDataViewsPromptProvider {...services}>
<LegacyServicesProvider {...getLegacyServices(services)}>{children}</LegacyServicesProvider>
</NoDataViewsPromptProvider>
</KibanaNoDataPageContext.Provider>
);
/**
* An interface containing a collection of Kibana plugins and services required to
* render this component and its dependencies.
*/
export interface KibanaNoDataPageKibanaDependencies {
coreStart: {
application: {
capabilities: {
navLinks: Record<string, boolean>;
};
currentAppId$: Observable<string | undefined>;
navigateToUrl: (url: string) => Promise<void>;
};
chrome: {
setIsVisible: (isVisible: boolean) => void;
};
docLinks: {
links: {
indexPatterns: {
introduction: string;
};
};
};
http: {
basePath: {
prepend: (url: string) => string;
};
};
};
dataViews: {
hasData: {
hasDataView: () => Promise<boolean>;
hasESData: () => Promise<boolean>;
hasUserDataView: () => Promise<boolean>;
};
};
dataViewEditor: {
openEditor: (options: DataViewEditorOptions) => () => void;
userPermissions: {
editDataView: () => boolean;
};
};
}
/**
* Kibana-specific Provider that maps dependencies to services.
*/
export const KibanaNoDataPageKibanaProvider: FC<KibanaNoDataPageKibanaDependencies> = ({
children,
...dependencies
}) => {
const { coreStart, dataViewEditor, dataViews } = dependencies;
const value: KibanaNoDataPageServices = {
addBasePath: coreStart.http.basePath.prepend,
canAccessFleet: coreStart.application.capabilities.navLinks.integrations,
canCreateNewDataView: dataViewEditor.userPermissions.editDataView(),
currentAppId$: coreStart.application.currentAppId$,
dataViewsDocLink: coreStart.docLinks.links.indexPatterns?.introduction,
hasDataView: dataViews.hasData.hasDataView,
hasESData: dataViews.hasData.hasESData,
hasUserDataView: dataViews.hasData.hasUserDataView,
navigateToUrl: coreStart.application.navigateToUrl,
openDataViewEditor: dataViewEditor.openEditor,
setIsFullscreen: (isVisible: boolean) => coreStart.chrome.setIsVisible(isVisible),
};
return (
<KibanaNoDataPageContext.Provider value={value}>
<NoDataViewsPromptKibanaProvider {...dependencies}>
<LegacyServicesProvider {...getLegacyServices(value)}>{children}</LegacyServicesProvider>
</NoDataViewsPromptKibanaProvider>
</KibanaNoDataPageContext.Provider>
);
};
/**
* React hook for accessing pre-wired services.
*/
export function useServices() {
const context = useContext(KibanaNoDataPageContext);
if (!context) {
throw new Error(
'KibanaNoDataPageContext is missing. Ensure your component or React root is wrapped with KibanaNoDataPageContext.'
);
}
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

@ -1,14 +1,28 @@
**id:** sharedUX/Components/NoDataViewsPrompt
**slug:** /shared-ux/components/no-data-views
**title:** No Data Views
**summary:** A prompt to be displayed when there is data in Elasticsearch, but no data views
**tags:** ['shared-ux', 'component']
**date:** 2022-02-09
---
id: sharedUX/Prompt/NoDataViews
slug: /shared-ux/prompt/no-data-views
title: No Data Views Prompt
summary: A prompt to be displayed when there is data in Elasticsearch, but no data views
tags: ['shared-ux', 'component', 'prompt', 'no-data']
date: 2022-02-09
---
When there is data in Elasticsearch, but there haven't been any data views created yet, we want to display an appropriate message to the user and facilitate creation of data views (if appropriate permissions are in place).
When there is data in Elasticsearch, but there haven't been any data views created yet, we want to display an appropriate message to the user and facilitate creation of data views, (if appropriate permissions are in place).
The pure component, `no_data_views.component.tsx`, is a pre-configured **EuiEmptyPrompt**.
The `NoDataViewsPrompt` connected component uses:
The connected component, `no_data_views.tsx`, uses services from the `shared_ux` plugin to open Data View Editor. You must wrap your plugin app in the `ServicesContext` provided by the start contract of the `shared_ux` plugin to use it.
- `userPermissions.editDataView` from `data_view_editor` to determine if the user has permission to create data views
- `openEditor` from `data_view_editor` to facilitate creating a data view.
- `docLinks` from `coreStart` to populate documentation links.
## API
| Export | Description |
|---|---|
| `NoDataViewsPromptProvider` | Provides contextual services to `NoDataViewsPrompt`. |
| `NoDataViewsPromptKibanaProvider` | Maps Kibana dependencies to provide contextual services to `NoDataViewsPrompt`. |
| `NoDataViewsPrompt` | Uses a `Provider` to access contextual services to populate props on the `NoDataViewsPromptComponent`. |
| `NoDataViewsPromptComponent` | The pure component, a pre-configured **EuiEmptyPrompt**. |
## EUI Promotion Status
This component is not currently considered for promotion to EUI.

View file

@ -6,7 +6,13 @@
* Side Public License, v 1.
*/
export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './services';
export type { NoDataViewsPromptKibanaServices, NoDataViewsPromptServices } from './services';
export { NoDataViewsPrompt } from './no_data_views';
export { NoDataViewsPrompt as NoDataViewsPromptComponent } from './no_data_views.component';
export { NoDataViewsPromptKibanaProvider, NoDataViewsPromptProvider } from './services';
export type { NoDataViewsPromptKibanaServices, NoDataViewsPromptServices } from './services';
export {
getMockServices as getNoDataViewsPromptMockServices,
getStoryArgTypes as getNoDataViewsPromptStoryArgTypes,
getStoryServices as getNoDataViewsPromptStorybookServices,
} from './mocks';

View file

@ -0,0 +1,48 @@
/*
* 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 { NoDataViewsPromptServices } from './services';
export type Params = Record<keyof ReturnType<typeof getStoryArgTypes>, any>;
export const getStoryServices = (params: Params) => {
const services: NoDataViewsPromptServices = {
...params,
openDataViewEditor: (options) => {
action('openDataViewEditor')(options);
return () => {};
},
};
return services;
};
export const getStoryArgTypes = () => ({
canCreateNewDataView: {
control: 'boolean',
defaultValue: true,
},
dataViewsDocLink: {
options: ['some/link', undefined],
control: { type: 'radio' },
},
});
export const getMockServices = (params?: Params) => {
const { canCreateNewDataView, dataViewsDocLink } = params || {};
const services: NoDataViewsPromptServices = {
canCreateNewDataView: canCreateNewDataView || true,
dataViewsDocLink: dataViewsDocLink || 'some/link',
openDataViewEditor: jest.fn(),
};
return services;
};

View file

@ -17,9 +17,13 @@ import { withSuspense } from '@kbn/shared-ux-utility';
import { DocumentationLink } from './documentation_link';
export interface Props {
/** True if the user has permission to create a data view, false otherwise. */
canCreateNewDataView: boolean;
/** Click handler for create button. **/
onClickCreate?: () => void;
/** Link to documentation on data views. */
dataViewsDocLink?: string;
/** The background color of the prompt; defaults to `plain`. */
emptyPromptColor?: EuiEmptyPromptProps['color'];
}

View file

@ -8,14 +8,17 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { NoDataViewsPrompt as NoDataViewsPromptComponent, Props } from './no_data_views.component';
import { NoDataViewsPrompt as NoDataViewsPromptComponent } from './no_data_views.component';
import { NoDataViewsPrompt } from './no_data_views';
import { NoDataViewsPromptProvider, NoDataViewsPromptServices } from './services';
import { NoDataViewsPromptProvider } from './services';
import mdx from '../README.mdx';
import { Params, getStoryArgTypes, getStoryServices } from './mocks';
const argTypes = getStoryArgTypes();
export default {
title: 'No Data/No Data Views',
title: 'No Data/Prompt',
description: 'A component to display when there are no user-created data views available.',
parameters: {
docs: {
@ -24,45 +27,36 @@ export default {
},
};
type ConnectedParams = Pick<NoDataViewsPromptServices, 'canCreateNewDataView' | 'dataViewsDocLink'>;
const openDataViewEditor: NoDataViewsPromptServices['openDataViewEditor'] = (options) => {
action('openDataViewEditor')(options);
return () => {};
};
export const ConnectedComponent = (params: ConnectedParams) => {
export const NoDataViews = (params: Params) => {
return (
<NoDataViewsPromptProvider {...{ openDataViewEditor, ...params }}>
<NoDataViewsPromptProvider {...getStoryServices(params)}>
<NoDataViewsPrompt onDataViewCreated={action('onDataViewCreated')} />
</NoDataViewsPromptProvider>
);
};
ConnectedComponent.argTypes = {
canCreateNewDataView: {
control: 'boolean',
defaultValue: true,
},
dataViewsDocLink: {
options: ['some/link', undefined],
control: { type: 'radio' },
NoDataViews.argTypes = argTypes;
const componentArgTypes = {
...argTypes,
emptyPromptColor: {
options: [
'plain',
'transparent',
'subdued',
'accent',
'primary',
'success',
'warning',
'danger',
],
control: { type: 'select' },
defaultValue: 'plain',
},
};
type PureParams = Pick<Props, 'canCreateNewDataView' | 'dataViewsDocLink'>;
export const PureComponent = (params: PureParams) => {
export const NoDataViewsComponent = (params: Record<keyof typeof componentArgTypes, any>) => {
return <NoDataViewsPromptComponent onClickCreate={action('onClick')} {...params} />;
};
PureComponent.argTypes = {
canCreateNewDataView: {
control: 'boolean',
defaultValue: true,
},
dataViewsDocLink: {
options: ['some/link', undefined],
control: { type: 'radio' },
},
};
NoDataViewsComponent.argTypes = componentArgTypes;

View file

@ -14,19 +14,14 @@ import { EuiButton } from '@elastic/eui';
import { NoDataViewsPrompt } from './no_data_views';
import { NoDataViewsPromptServices, NoDataViewsPromptProvider } from './services';
const getServices = (canCreateNewDataView: boolean = true) => ({
canCreateNewDataView,
openDataViewEditor: jest.fn(),
dataViewsDocLink: 'some/link',
});
import { getMockServices } from './mocks';
describe('<NoDataViewsPromptTest />', () => {
let services: NoDataViewsPromptServices;
let mount: (element: JSX.Element) => ReactWrapper;
beforeEach(() => {
services = getServices();
services = getMockServices();
mount = (element: JSX.Element) =>
mountWithIntl(<NoDataViewsPromptProvider {...services}>{element}</NoDataViewsPromptProvider>);
});

View file

@ -13,6 +13,7 @@ import { useServices, NoDataViewsPromptServices } from './services';
// TODO: https://github.com/elastic/kibana/issues/127695
export interface Props {
/** Handler for successfully creating a new data view. */
onDataViewCreated: (dataView: unknown) => void;
}
@ -22,10 +23,7 @@ type CloseDataViewEditorFn = ReturnType<NoDataViewsPromptServices['openDataViewE
* A service-enabled component that provides Kibana-specific functionality to the `NoDataViewsPrompt`
* component.
*
* Use of this component requires both the `EuiTheme` context as well as either a configured Shared UX
* `ServicesProvider` or the `ServicesContext` provided by the Shared UX public plugin contract.
*
* See shared-ux/public/services for information.
* Use of this component requires both the `EuiTheme` context as well as a `NoDataViewsPrompt` provider.
*/
export const NoDataViewsPrompt = ({ onDataViewCreated }: Props) => {
const { canCreateNewDataView, openDataViewEditor, dataViewsDocLink } = useServices();

View file

@ -51,8 +51,14 @@ export const NoDataViewsPromptProvider: FC<NoDataViewsPromptServices> = ({
children,
...services
}) => {
// Typescript types are widened to accept more than what is needed. Take only what is necessary
// so the context remains clean.
const { canCreateNewDataView, dataViewsDocLink, openDataViewEditor } = services;
return (
<NoDataViewsPromptContext.Provider value={services}>
<NoDataViewsPromptContext.Provider
value={{ canCreateNewDataView, dataViewsDocLink, openDataViewEditor }}
>
{children}
</NoDataViewsPromptContext.Provider>
);

View file

@ -3319,6 +3319,10 @@
version "0.0.0"
uid ""
"@kbn/shared-ux-page-kibana-no-data@link:bazel-bin/packages/shared-ux/page/kibana_no_data":
version "0.0.0"
uid ""
"@kbn/shared-ux-prompt-no-data-views@link:bazel-bin/packages/shared-ux/prompt/no_data_views":
version "0.0.0"
uid ""
@ -6638,6 +6642,10 @@
version "0.0.0"
uid ""
"@types/kbn__shared-ux-page-kibana-no-data@link:bazel-bin/packages/shared-ux/page/kibana_no_data/npm_module_types":
version "0.0.0"
uid ""
"@types/kbn__shared-ux-prompt-no-data-views@link:bazel-bin/packages/shared-ux/prompt/no_data_views/npm_module_types":
version "0.0.0"
uid ""