diff --git a/dev_docs/assets/kibana_template_no_data_config.png b/dev_docs/assets/kibana_template_no_data_config.png new file mode 100644 index 000000000000..5e54bfdce193 Binary files /dev/null and b/dev_docs/assets/kibana_template_no_data_config.png differ diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index d9605ac5643b..eab5b2eb3ce8 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -1,7 +1,7 @@ --- id: kibDevDocsKPTTutorial slug: /kibana-dev-docs/tutorials/kibana-page-template -title: KibanaPageTemplate component +title: Kibana Page Template summary: Learn how to create pages in Kibana date: 2021-03-20 tags: ['kibana', 'dev', 'ui', 'tutorials'] @@ -117,3 +117,54 @@ When using `EuiSideNav`, root level items should not be linked but provide secti ![Screenshot of Stack Management empty state with a provided solution navigation shown on the left, outlined in pink.](../assets/kibana_template_solution_nav.png) ![Screenshots of Stack Management page in mobile view. Menu closed on the left, menu open on the right.](../assets/kibana_template_solution_nav_mobile.png) + +## `noDataConfig` + +Increases the consistency in messaging across all the solutions during the getting started process when no data exists. Each solution/template instance decides when is the most appropriate time to show this configuration, but is messaged specifically towards having no indices or index patterns at all or that match the particular solution. + +This is a built-in configuration that displays a very specific UI and requires very specific keys. It will also ignore all other configurations of the template including `pageHeader` and `children`, with the exception of continuing to show `solutionNav`. + +The `noDataConfig` is of type [`NoDataPagProps`](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx): + +1. `solution: string`: Single name for the current solution, used to auto-generate the title, logo, description, and button label *(required)* +2. `docsLink: string`: Required to set the docs link for the whole solution *(required)* +3. `logo?: string`: Optionally replace the auto-generated logo +4. `pageTitle?: string`: Optionally replace the auto-generated page title (h1) +5. `actions: NoDataPageActionsProps`: An object of `NoDataPageActions` configurations with unique primary keys *(required)* + +### `NoDataPageActions` + +There are two main actions for adding data that we promote throughout Kibana, Elastic Agent and Beats. They are added to the cards that are displayed by using the keys `elasticAgent` and `beats` respectively. For consistent messaging, these two cards are pre-configured but require specific `href`s and/or `onClick` handlers for directing the user to the right location for that solution. + +It also accepts a `recommended` prop as a boolean to promote one or more of the cards through visuals added to the UI. It will also place the `recommended` ones first in the list. By default, the configuration will recommend `elasticAgent`. Optionally you can also replace the `button` label by passing a string, or the whole component by passing a `ReactNode`. + + +```tsx +// Perform your own check +const hasData = checkForData(); + +// No data configuration +const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { + solution: 'Observability', + docsLink: '#', + actions: { + elasticAgent: { + href: '#', + }, + beats: { + href: '#', + }, + }, +}; + + + {/* Children will be ignored */} + +``` + + +![Screenshot of and example in Observability using the no data configuration and using the corresponding list numbers to point out the UI elements that they adjust.](../assets/kibana_template_no_data_config.png) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 4772e00d5645..48130a7bfcf5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -42,7 +42,7 @@ pageLoadAssetSize: inspector: 148711 kibanaLegacy: 107711 kibanaOverview: 56279 - kibanaReact: 161921 + kibanaReact: 188705 kibanaUtils: 198829 lens: 96624 licenseManagement: 41817 diff --git a/src/plugins/kibana_react/public/assets/elastic_agent_card.svg b/src/plugins/kibana_react/public/assets/elastic_agent_card.svg new file mode 100644 index 000000000000..82d2bf854186 --- /dev/null +++ b/src/plugins/kibana_react/public/assets/elastic_agent_card.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg new file mode 100644 index 000000000000..8652d8d92150 --- /dev/null +++ b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg new file mode 100644 index 000000000000..f54786c1b950 --- /dev/null +++ b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/texture.svg b/src/plugins/kibana_react/public/assets/texture.svg new file mode 100644 index 000000000000..fea0d6954343 --- /dev/null +++ b/src/plugins/kibana_react/public/assets/texture.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap index 9ad2bd73674b..d90daa33d168 100644 --- a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap @@ -2,6 +2,7 @@ exports[`KibanaPageTemplate render basic template 1`] = ` `; +exports[`KibanaPageTemplate render noDataContent 1`] = ` + + } + pageSideBarProps={ + Object { + "className": "kbnPageTemplate__pageSideBar", + "paddingSize": "none", + } + } + restrictWidth={950} + template="centeredBody" +> + + +`; + exports[`KibanaPageTemplate render solutionNav 1`] = ` + + + +

+ +

+ +

+ + + , + "solution": "Elastic", + } + } + /> +

+
+
+ + + + + + + + + + + + + + + +

+ + + , + } + } + /> +

+
+ +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts new file mode 100644 index 000000000000..55661ad6f14f --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/index.ts @@ -0,0 +1,10 @@ +/* + * 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 * from './no_data_page'; +export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap new file mode 100644 index 000000000000..c8fda1d03643 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ElasticAgentCard props button 1`] = ` + + Button + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard props href 1`] = ` + + Button + + } + href="#" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard props recommended 1`] = ` + + Find an integration for Solution + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; + +exports[`ElasticAgentCard renders 1`] = ` + + Find an integration for Solution + + } + href="app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title="Add a Solution integration" +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap new file mode 100644 index 000000000000..1146e4f676eb --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ElasticBeatsCard props button 1`] = ` + + Button + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard props href 1`] = ` + + Button + + } + href="#" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard props recommended 1`] = ` + + Install Beats for Solution + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; + +exports[`ElasticBeatsCard renders 1`] = ` + + Install Beats for Solution + + } + href="app/home#/tutorial" + image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" + paddingSize="l" + title="Add data with Beats" +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap new file mode 100644 index 000000000000..a8232c209ed7 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/no_data_card.test.tsx.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoDataCard props button 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+ +
+`; + +exports[`NoDataCard props href 1`] = ` +
+
+ + + Card title + + +
+

+ Description +

+
+
+ +
+`; + +exports[`NoDataCard props recommended 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+ + + Recommended + + +
+`; + +exports[`NoDataCard renders 1`] = ` +
+
+ + Card title + +
+

+ Description +

+
+
+
+`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx new file mode 100644 index 000000000000..45cc32cae06d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -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 { shallow } from 'enzyme'; +import React from 'react'; +import { ElasticAgentCard } from './elastic_agent_card'; + +jest.mock('../../../context', () => ({ + ...jest.requireActual('../../../context'), + useKibana: jest.fn().mockReturnValue({ + services: { + http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + uiSettings: { get: jest.fn() }, + }, + }), +})); + +describe('ElasticAgentCard', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx new file mode 100644 index 000000000000..f0ee2fc2739d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -0,0 +1,69 @@ +/* + * 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. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { EuiButton, EuiCard } from '@elastic/eui'; +import { useKibana } from '../../../context'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +export type ElasticAgentCardProps = NoDataPageActions & { + solution: string; +}; + +/** + * Applies extra styling to a typical EuiAvatar + */ +export const ElasticAgentCard: FunctionComponent = ({ + solution, + recommended = true, + href = 'app/integrations/browse', + button, + ...cardRest +}) => { + const { + services: { http }, + } = useKibana(); + const addBasePath = http.basePath.prepend; + const basePathUrl = '/plugins/kibanaReact/assets/'; + + const footer = + typeof button !== 'string' && typeof button !== 'undefined' ? ( + button + ) : ( + + {button || + i18n.translate('kibana-react.noDataPage.elasticAgentCard.buttonLabel', { + defaultMessage: 'Find an integration for {solution}', + values: { solution }, + })} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx new file mode 100644 index 000000000000..6ea41bf6b3e1 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx @@ -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 { shallow } from 'enzyme'; +import React from 'react'; +import { ElasticBeatsCard } from './elastic_beats_card'; + +jest.mock('../../../context', () => ({ + ...jest.requireActual('../../../context'), + useKibana: jest.fn().mockReturnValue({ + services: { + http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + uiSettings: { get: jest.fn() }, + }, + }), +})); + +describe('ElasticBeatsCard', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx new file mode 100644 index 000000000000..cf147315c97f --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { EuiButton, EuiCard } from '@elastic/eui'; +import { useKibana } from '../../../context'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +export type ElasticBeatsCardProps = NoDataPageActions & { + solution: string; +}; + +export const ElasticBeatsCard: FunctionComponent = ({ + recommended, + href = 'app/home#/tutorial', + button, + solution, + ...cardRest +}) => { + const { + services: { http, uiSettings }, + } = useKibana(); + const addBasePath = http.basePath.prepend; + const basePathUrl = '/plugins/kibanaReact/assets/'; + const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + + const footer = + typeof button !== 'string' && typeof button !== 'undefined' ? ( + button + ) : ( + + {button || + i18n.translate('kibana-react.noDataPage.elasticBeatsCard.buttonLabel', { + defaultMessage: 'Install Beats for {solution}', + values: { solution }, + })} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts new file mode 100644 index 000000000000..3744239d9a47 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './elastic_agent_card'; +export * from './elastic_beats_card'; +export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx new file mode 100644 index 000000000000..a809ede2dc61 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.test.tsx @@ -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 { render } from 'enzyme'; +import React from 'react'; +import { NoDataCard } from './no_data_card'; + +describe('NoDataCard', () => { + test('renders', () => { + const component = render(); + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + test('recommended', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + + test('button', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + + test('href', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx new file mode 100644 index 000000000000..0be85f8c8ed1 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/no_data_card.tsx @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import React, { FunctionComponent } from 'react'; +import { EuiButton, EuiCard, EuiCardProps } from '@elastic/eui'; +import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; + +// Custom cards require all the props the EuiCard does +type NoDataCard = EuiCardProps & NoDataPageActions; + +export const NoDataCard: FunctionComponent = ({ + recommended, + button, + ...cardRest +}) => { + const footer = + typeof button !== 'string' ? ( + button + ) : ( + + {button} + + ); + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss new file mode 100644 index 000000000000..f1bc12e74cf4 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.scss @@ -0,0 +1,7 @@ +.kbnNoDataPageContents__item:only-child { + min-width: 400px; + + @include euiBreakpoint('xs', 's') { + min-width: auto; + } +} diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx new file mode 100644 index 000000000000..59d6e8280af9 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { NoDataPage } from './no_data_page'; +import { shallowWithIntl } from '@kbn/test/jest'; + +describe('NoDataPage', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx new file mode 100644 index 000000000000..56eb0f34617d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -0,0 +1,203 @@ +/* + * 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 './no_data_page.scss'; + +import React, { ReactNode, useMemo, FunctionComponent, MouseEventHandler } from 'react'; +import { + EuiFlexItem, + EuiCardProps, + EuiFlexGrid, + EuiSpacer, + EuiText, + EuiTextColor, + EuiLink, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaPageTemplateProps } from '../page_template'; + +import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card'; +import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; + +export const NO_DATA_PAGE_MAX_WIDTH = 950; +export const NO_DATA_PAGE_TEMPLATE_PROPS: KibanaPageTemplateProps = { + restrictWidth: NO_DATA_PAGE_MAX_WIDTH, + template: 'centeredBody', + pageContentProps: { + hasShadow: false, + color: 'transparent', + }, +}; + +export const NO_DATA_RECOMMENDED = i18n.translate( + 'kibana-react.noDataPage.noDataPage.recommended', + { + defaultMessage: 'Recommended', + } +); + +export type NoDataPageActions = Partial & { + /** + * Applies the `Recommended` beta badge and makes the button `fill` + */ + recommended?: boolean; + /** + * Provide just a string for the button's label, or a whole component + */ + button?: string | ReactNode; + /** + * Remapping `onClick` to any element + */ + onClick?: MouseEventHandler; +}; + +export type NoDataPageActionsProps = Record; + +export interface NoDataPageProps { + /** + * Single name for the current solution, used to auto-generate the title, logo, description, and button label + */ + solution: string; + /** + * Optionally replace the auto-generated logo + */ + logo?: string; + /** + * Required to set the docs link for the whole solution + */ + docsLink: string; + /** + * Optionally replace the auto-generated page title (h1) + */ + pageTitle?: string; + /** + * An object of `NoDataPageActions` configurations with unique primary keys. + * Use `elasticAgent` or `beats` as the primary key for pre-configured cards of this type. + * Otherwise use a custom key that contains `EuiCard` props. + */ + actions: NoDataPageActionsProps; +} + +export const NoDataPage: FunctionComponent = ({ + solution, + logo, + actions, + docsLink, + pageTitle, +}) => { + // Convert obj data into an iterable array + const entries = Object.entries(actions); + + // This sort fn may look nonsensical, but it's some Good Ol' Javascript (TM) + // Sort functions want either a 1, 0, or -1 returned to determine order, + // and it turns out in JS you CAN minus booleans from each other to get a 1, 0, or -1 - e.g., (true - false == 1) :whoa: + const sortedEntries = entries.sort(([, firstObj], [, secondObj]) => { + // The `??` fallbacks are because the recommended key can be missing or undefined + return Number(secondObj.recommended ?? false) - Number(firstObj.recommended ?? false); + }); + + // Convert the iterated [[key, value]] array format back into an object + const sortedData = Object.fromEntries(sortedEntries); + const actionsKeys = Object.keys(sortedData); + const renderActions = useMemo(() => { + return Object.values(sortedData).map((action, i) => { + if (actionsKeys[i] === 'elasticAgent') { + return ( + + + + ); + } else if (actionsKeys[i] === 'beats') { + return ( + + + + ); + } else { + return ( + + + + ); + } + }); + }, [actions, sortedData, actionsKeys]); + + return ( +
+ + + +

+ {pageTitle || ( + + )} +

+ +

+ + + + ), + }} + /> +

+
+
+ + + + {renderActions} + + {actionsKeys.length > 1 ? ( + <> + + +

+ + + + ), + }} + /> +

+
+ + ) : undefined} +
+ ); +}; diff --git a/src/plugins/kibana_react/public/page_template/page_template.scss b/src/plugins/kibana_react/public/page_template/page_template.scss index 631511cd0475..6b1c17e870e8 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.scss +++ b/src/plugins/kibana_react/public/page_template/page_template.scss @@ -10,4 +10,8 @@ &.kbnPageTemplate__pageSideBar--shrink { min-width: $euiSizeXXL; } + + .kbnPageTemplate--centeredBody & { + border-right: $euiBorderThin; + } } diff --git a/src/plugins/kibana_react/public/page_template/page_template.test.tsx b/src/plugins/kibana_react/public/page_template/page_template.test.tsx index 2fdedce23b09..6c6c4bb33e6b 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.test.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { KibanaPageTemplate } from './page_template'; +import { KibanaPageTemplate, KibanaPageTemplateProps } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; @@ -51,6 +51,16 @@ const navItems: KibanaPageTemplateSolutionNavProps['items'] = [ }, ]; +const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { + solution: 'Elastic', + actions: { + elasticAgent: {}, + beats: {}, + custom: {}, + }, + docsLink: 'test', +}; + describe('KibanaPageTemplate', () => { test('render default empty prompt', () => { const component = shallow( @@ -126,6 +136,26 @@ describe('KibanaPageTemplate', () => { expect(component).toMatchSnapshot(); }); + test('render noDataContent', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + test('render sidebar classes', () => { const component = shallow( = ({ template, + className, pageHeader, children, isEmptyState, @@ -50,6 +58,7 @@ export const KibanaPageTemplate: FunctionComponent = ({ pageSideBar, pageSideBarProps, solutionNav, + noDataConfig, ...rest }) => { /** @@ -86,11 +95,10 @@ export const KibanaPageTemplate: FunctionComponent = ({ ); } - const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; - /** * An easy way to create the right content for empty pages */ + const emptyStateDefaultTemplate = pageSideBar ? 'centeredContent' : 'centeredBody'; if (isEmptyState && pageHeader && !children) { template = template ?? emptyStateDefaultTemplate; const { iconType, pageTitle, description, rightSideItems } = pageHeader; @@ -110,9 +118,40 @@ export const KibanaPageTemplate: FunctionComponent = ({ template = template ?? emptyStateDefaultTemplate; } + // Set the template before the classes + template = noDataConfig ? NO_DATA_PAGE_TEMPLATE_PROPS.template : template; + + const classes = classNames( + 'kbnPageTemplate', + { [`kbnPageTemplate--${template}`]: template }, + className + ); + + /** + * If passing the custom template of `noDataConfig` + */ + if (noDataConfig) { + return ( + + + + ); + } + return ( & { + /** + * Any EuiAvatar size available, of `xxl` for custom large, brand-focused version + */ + size?: EuiAvatarProps['size'] | 'xxl'; +}; /** * Applies extra styling to a typical EuiAvatar */ export const KibanaPageTemplateSolutionNavAvatar: FunctionComponent = ({ className, + size, ...rest }) => { return ( + // @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine ); diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index 02f54723abd4..8359b186e4af 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -2,7 +2,7 @@ exports[`Storyshots Home Home Page 1`] = `