diff --git a/CHANGELOG.md b/CHANGELOG.md index 623bdc9d71f..7fa0ba8f274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ - Added `color` and `size` props and added support for click event to `EuiBetaBadge` ([#4798](https://github.com/elastic/eui/pull/4798)) - Added `documentation` and `layers` glyphs to `EuiIcon` ([#4833](https://github.com/elastic/eui/pull/4833)) - Updated `EuiTourStep`'s `title` and `subtitle` prop type from `string` to `ReactNode` ([#4841](https://github.com/elastic/eui/pull/4841)) +- Added `euiCantAnimate` Sass mixin ([#4835](https://github.com/elastic/eui/pull/4835)) +- Added new `EuiLoadingLogo` component ([#4835](https://github.com/elastic/eui/pull/4835)) +- Added `icon` props to `EuiEmptyPrompt` for custom icons ([#4835](https://github.com/elastic/eui/pull/4835)) +- Deprecated `EuiLoadingKibana` ([#4835](https://github.com/elastic/eui/pull/4135)) +- Paused animations when `prefers-reduced-motion` is on for loader components ([#4835](https://github.com/elastic/eui/pull/4135)) **Bug fixes** diff --git a/src-docs/src/views/empty_prompt/empty_prompt_error.js b/src-docs/src/views/empty_prompt/empty_prompt_error.js new file mode 100644 index 00000000000..ff19edd6e7d --- /dev/null +++ b/src-docs/src/views/empty_prompt/empty_prompt_error.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import { EuiEmptyPrompt } from '../../../../src/components'; + +export default () => ( + Error loading Dashboards} + body={ +

+ There was an error loading the Dashboard application. Contact your + administrator for help. +

+ } + /> +); diff --git a/src-docs/src/views/empty_prompt/empty_prompt_example.js b/src-docs/src/views/empty_prompt/empty_prompt_example.js index ad8b35cf2d8..d44b7799b9d 100644 --- a/src-docs/src/views/empty_prompt/empty_prompt_example.js +++ b/src-docs/src/views/empty_prompt/empty_prompt_example.js @@ -1,6 +1,5 @@ import React, { Fragment } from 'react'; - -import { renderToHtml } from '../../services'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; @@ -10,7 +9,6 @@ import emptyPromptConfig from './playground'; import EmptyPrompt from './empty_prompt'; const emptyPromptSource = require('!!raw-loader!./empty_prompt'); -const emptyPromptHtml = renderToHtml(EmptyPrompt); const emptyPromptSnippet = `You have no spice} @@ -20,7 +18,6 @@ const emptyPromptSnippet = `You have no spice} @@ -31,12 +28,29 @@ const customSnippet = `You have no spice} actions={multipleActions} />`; +import Loading from './empty_prompt_loading'; +const loadingSource = require('!!raw-loader!./empty_prompt_loading'); +const loadingSnippet = `} + title={

Loading

} +/>`; + +import Error from './empty_prompt_error'; +const errorSource = require('!!raw-loader!./empty_prompt_error'); +const errorSnippet = `There was an error} +/>`; + +import States from './empty_prompt_states'; +const statesSource = require('!!raw-loader!./empty_prompt_states'); + export const EmptyPromptExample = { title: 'Empty prompt', sections: [ @@ -46,20 +60,18 @@ export const EmptyPromptExample = { type: GuideSectionTypes.JS, code: emptyPromptSource, }, - { - type: GuideSectionTypes.HTML, - code: emptyPromptHtml, - }, ], text: (

- Use the EuiEmptyPrompt as a placeholder for an empty - table or list of content. + Use the EuiEmptyPrompt as a placeholder for any type + of empty content. They are especially helpful for replacing entire + pages that contain no content.

), props: { EuiEmptyPrompt }, demo: , snippet: emptyPromptSnippet, + playground: emptyPromptConfig, }, { title: 'Custom sizes and colors', @@ -68,15 +80,12 @@ export const EmptyPromptExample = { type: GuideSectionTypes.JS, code: customSource, }, - { - type: GuideSectionTypes.HTML, - code: customHtml, - }, ], text: (

- You can control sizes and colors with the iconColor - , and titleSize props. + You can control the title size and icon color with the{' '} + titleSize and iconColor props + respectively.

), props: { EuiEmptyPrompt }, @@ -90,17 +99,13 @@ export const EmptyPromptExample = { type: GuideSectionTypes.JS, code: simpleSource, }, - { - type: GuideSectionTypes.HTML, - code: simpleHtml, - }, ], text: ( -

You can remove parts of the prompt to simplify it, if you wish.

+

You can remove parts of the prompt to simplify it.

You can also provide an array of multiple actions. Be sure to list - primary actions first and secondary actions last. + primary actions first and secondary actions as empty buttons.

), @@ -108,6 +113,84 @@ export const EmptyPromptExample = { demo: , snippet: simpleSnippet, }, + { + title: 'Loading and error prompts', + source: [ + { + type: GuideSectionTypes.JS, + code: loadingSource, + }, + ], + text: ( + <> +

+ Empty prompts can also be used to emulate loading and error states, + by utilizing the same patterns. +

+

+ For loading states, you can simply replace the{' '} + iconType with a custom icon by + passing in one of our{' '} + loading components. +

+ + ), + props: { EuiEmptyPrompt }, + demo: , + snippet: loadingSnippet, + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: errorSource, + }, + ], + text: ( + <> +

+ For error states, you can simply set the{' '} + iconColor to danger and/or + wrap the whole prompt in a danger colored{' '} + + EuiPanel + + . +

+ + ), + props: { EuiEmptyPrompt }, + demo: , + snippet: errorSnippet, + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: statesSource, + }, + ], + text: ( + <> +

+ You can then tie all three states together to create a seamless + loading to empty or loading to error experience. The following + example shows how to encorprate these states with{' '} + + EuiPageTemplate + {' '} + using {'template="centeredContent"'} and passing{' '} + {'color="danger"'} to the{' '} + pageContentProps for the error state. +

+ + ), + props: { EuiEmptyPrompt }, + demo: ( +
+ +
+ ), + }, ], - playground: emptyPromptConfig, }; diff --git a/src-docs/src/views/empty_prompt/empty_prompt_loading.js b/src-docs/src/views/empty_prompt/empty_prompt_loading.js new file mode 100644 index 00000000000..f21677330bf --- /dev/null +++ b/src-docs/src/views/empty_prompt/empty_prompt_loading.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import { EuiEmptyPrompt, EuiLoadingLogo } from '../../../../src/components'; + +export default () => ( + } + title={

Loading Dashboards

} + /> +); diff --git a/src-docs/src/views/empty_prompt/empty_prompt_states.js b/src-docs/src/views/empty_prompt/empty_prompt_states.js new file mode 100644 index 00000000000..57c24921019 --- /dev/null +++ b/src-docs/src/views/empty_prompt/empty_prompt_states.js @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; + +import { + EuiEmptyPrompt, + EuiPageTemplate, + EuiLoadingLogo, + EuiButton, +} from '../../../../src/components'; + +export default () => { + const states = ['loading1', 'error', 'loading2', 'empty']; + + const [currentState, setCurrentState] = useState(states[0]); + + const searchTimeout = setTimeout(() => { + // Cycle through the array of states + const index = states.indexOf(currentState); + setCurrentState(index < states.length - 1 ? states[index + 1] : states[0]); + }, 3000); + + useEffect(() => { + return () => { + clearTimeout(searchTimeout); + }; + }); + + let emptyPromptProps; + switch (currentState) { + case 'error': + emptyPromptProps = { + iconType: 'alert', + iconColor: 'danger', + title:

Error loading Dashboards

, + body: ( +

+ There was an error loading the Dashboard application. Contact your + administrator for help. +

+ ), + }; + break; + case 'empty': + emptyPromptProps = { + iconType: 'dashboardApp', + iconColor: 'default', + title:

Dashboards

, + body:

You don't have any dashboards yet.

, + actions: [ + + Create new dashboard + , + ], + }; + break; + + default: + emptyPromptProps = { + icon: , + title:

Loading Dashboards

, + }; + break; + } + + return ( + + + + ); +}; diff --git a/src-docs/src/views/empty_prompt/playground.js b/src-docs/src/views/empty_prompt/playground.js index 8a707f93763..15033cd8146 100644 --- a/src-docs/src/views/empty_prompt/playground.js +++ b/src-docs/src/views/empty_prompt/playground.js @@ -13,7 +13,7 @@ export default () => { propsToUse.title = { ...propsToUse.title, - value: '<>You have no spice', + value: '

You have no spice

', type: PropTypes.ReactNode, }; @@ -36,10 +36,9 @@ export default () => { propsToUse.body.type = PropTypes.String; propsToUse.body.value = `Navigators use massive amounts of spice to gain a limited form of prescience. This allows them to safely navigate interstellar space, - enabling trade and travel throughout the galaxy. - `; + enabling trade and travel throughout the galaxy.`; - propsToUse.iconType = iconValidator(propsToUse.iconType); + propsToUse.iconType = iconValidator(propsToUse.iconType, 'editorStrike'); return { config: { diff --git a/src-docs/src/views/loading/loading_elastic.tsx b/src-docs/src/views/loading/loading_elastic.tsx index 6b589fea615..7c237fdf4f7 100644 --- a/src-docs/src/views/loading/loading_elastic.tsx +++ b/src-docs/src/views/loading/loading_elastic.tsx @@ -4,9 +4,12 @@ import { EuiLoadingElastic } from '../../../../src/components/loading'; export default () => (
- + +   +   +  
); diff --git a/src-docs/src/views/loading/loading_example.js b/src-docs/src/views/loading/loading_example.js index fc2d1085d7c..4f638c43d9c 100644 --- a/src-docs/src/views/loading/loading_example.js +++ b/src-docs/src/views/loading/loading_example.js @@ -1,12 +1,12 @@ import React from 'react'; - -import { renderToHtml } from '../../services'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; import { EuiCode, - EuiLoadingKibana, + EuiText, + EuiLoadingLogo, EuiLoadingElastic, EuiLoadingSpinner, EuiLoadingChart, @@ -15,33 +15,39 @@ import { import { loadingElasticConfig, loadingChartConfig, - loadingKibanaConfig, + loadingLogoConfig, loadingSpinnerConfig, loadingContentConfig, } from './playground'; -import LoadingKibana from './loading_kibana'; -const loadingKibanaSource = require('!!raw-loader!./loading_kibana'); -const loadingKibanaHtml = renderToHtml(LoadingKibana); +import LoadingLogo from './loading_kibana'; +const loadingLogoSource = require('!!raw-loader!./loading_kibana'); import LoadingElastic from './loading_elastic'; const loadingElasticSource = require('!!raw-loader!./loading_elastic'); -const loadingElasticHtml = renderToHtml(LoadingElastic); import LoadingChart from './loading_chart'; const loadingChartSource = require('!!raw-loader!./loading_chart'); -const loadingChartHtml = renderToHtml(LoadingChart); import LoadingSpinner from './loading_spinner'; const loadingSpinnerSource = require('!!raw-loader!./loading_spinner'); -const loadingSpinnerHtml = renderToHtml(LoadingSpinner); import LoadingContent from './loading_content'; const loadingContentSource = require('!!raw-loader!./loading_content'); -const loadingContentHtml = renderToHtml(LoadingContent); export const LoadingExample = { title: 'Loading', + intro: ( + +

+ Use loading indicators sparingly and opt for showing actual{' '} + progress over + infinite spinners. It is ok to use multiple loaders on a page if each + section is progressively loaded. However, if the entire page is loaded + at once, use a single, larger loading indicator. +

+
+ ), sections: [ { title: 'Elastic', @@ -50,42 +56,40 @@ export const LoadingExample = { type: GuideSectionTypes.JS, code: loadingElasticSource, }, - { - type: GuideSectionTypes.HTML, - code: loadingElasticHtml, - }, ], text: (

- Elastic logo based load. Should only be used in very large panels, - like bootup screens. + The EuiLoadingElastic loader is great for full page + or Elastic product loading screens.

), props: { EuiLoadingElastic }, demo: , snippet: '', + playground: loadingElasticConfig, }, { - title: 'Kibana', + title: 'Logos', source: [ { type: GuideSectionTypes.JS, - code: loadingKibanaSource, - }, - { - type: GuideSectionTypes.HTML, - code: loadingKibanaHtml, + code: loadingLogoSource, }, ], text: (

- Logo based loader. Should only be used in very large panels, like - bootup screens. + EuiLoadingLogo accepts any of our{' '} + + EuiIcon + {' '} + logos. It should only be used in very large panels, like full screen + pages.

), - props: { EuiLoadingKibana }, - demo: , - snippet: '', + props: { EuiLoadingLogo }, + demo: , + snippet: '', + playground: loadingLogoConfig, }, { title: 'Chart', @@ -94,22 +98,19 @@ export const LoadingExample = { type: GuideSectionTypes.JS, code: loadingChartSource, }, - { - type: GuideSectionTypes.HTML, - code: loadingChartHtml, - }, ], text: (

- Loader for the loading of chart or dashboard and visualization - elements. The colored versions should be used sparingly, only when a - single large visualization is loaded. When loading smaller groups of - panels, the smaller, mono versions should be used. + To indicate that a visualization is loading, use{' '} + EuiLoadingChart. The multi-color version should be + used sparingly, and only when a single large visualization is being + loaded.

), props: { EuiLoadingChart }, demo: , snippet: '', + playground: loadingChartConfig, }, { title: 'Spinner', @@ -118,15 +119,17 @@ export const LoadingExample = { type: GuideSectionTypes.JS, code: loadingSpinnerSource, }, - { - type: GuideSectionTypes.HTML, - code: loadingSpinnerHtml, - }, ], - text:

A simple spinner for most loading applications.

, + text: ( +

+ EuiLoadingSpinner is a simple spinner for most + loading contexts. +

+ ), props: { EuiLoadingSpinner }, demo: , snippet: '', + playground: loadingSpinnerConfig, }, { title: 'Text content', @@ -135,27 +138,18 @@ export const LoadingExample = { type: GuideSectionTypes.JS, code: loadingContentSource, }, - { - type: GuideSectionTypes.HTML, - code: loadingContentHtml, - }, ], text: (

- A simple loading animation for displaying placeholder text content. - You can pass in a number of lines between 1 and 10. + EuiLoadingContent is a simple loading animation for + displaying placeholder text content. You can pass in a number of{' '} + lines between 1 and 10.

), props: { EuiLoadingContent }, demo: , snippet: '', + playground: loadingContentConfig, }, ], - playground: [ - loadingElasticConfig, - loadingChartConfig, - loadingKibanaConfig, - loadingSpinnerConfig, - loadingContentConfig, - ], }; diff --git a/src-docs/src/views/loading/loading_kibana.tsx b/src-docs/src/views/loading/loading_kibana.tsx index 50c8c293851..68e67089063 100644 --- a/src-docs/src/views/loading/loading_kibana.tsx +++ b/src-docs/src/views/loading/loading_kibana.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { EuiLoadingKibana } from '../../../../src/components/loading'; +import { EuiLoadingLogo } from '../../../../src/components/loading'; export default () => (
- - - + +   + +   +
); diff --git a/src-docs/src/views/loading/playground.js b/src-docs/src/views/loading/playground.js index c7e7482ea98..fce4aa5653b 100644 --- a/src-docs/src/views/loading/playground.js +++ b/src-docs/src/views/loading/playground.js @@ -1,8 +1,11 @@ -import { propUtilityForPlayground } from '../../services/playground'; +import { + propUtilityForPlayground, + iconValidator, +} from '../../services/playground'; import { EuiLoadingElastic, EuiLoadingChart, - EuiLoadingKibana, + EuiLoadingLogo, EuiLoadingSpinner, EuiLoadingContent, } from '../../../../src/components/'; @@ -52,22 +55,23 @@ export const loadingChartConfig = () => { }; }; -export const loadingKibanaConfig = () => { - const docgenInfo = Array.isArray(EuiLoadingKibana.__docgenInfo) - ? EuiLoadingKibana.__docgenInfo[0] - : EuiLoadingKibana.__docgenInfo; +export const loadingLogoConfig = () => { + const docgenInfo = Array.isArray(EuiLoadingLogo.__docgenInfo) + ? EuiLoadingLogo.__docgenInfo[0] + : EuiLoadingLogo.__docgenInfo; const propsToUse = propUtilityForPlayground(docgenInfo.props); + propsToUse.logo = iconValidator(propsToUse.logo); return { config: { - componentName: 'EuiLoadingKibana', + componentName: 'EuiLoadingLogo', props: propsToUse, scope: { - EuiLoadingKibana, + EuiLoadingLogo, }, imports: { '@elastic/eui': { - named: ['EuiLoadingKibana'], + named: ['EuiLoadingLogo'], }, }, }, diff --git a/src/components/empty_prompt/__snapshots__/empty_prompt.test.tsx.snap b/src/components/empty_prompt/__snapshots__/empty_prompt.test.tsx.snap index faf354d4355..667ca374061 100644 --- a/src/components/empty_prompt/__snapshots__/empty_prompt.test.tsx.snap +++ b/src/components/empty_prompt/__snapshots__/empty_prompt.test.tsx.snap @@ -11,16 +11,16 @@ exports[`EuiEmptyPrompt is rendered 1`] = ` data-euiicon-type="arrowUp" />
+

+ Title +

-

- Title -

@@ -35,9 +35,6 @@ exports[`EuiEmptyPrompt is rendered 1`] = `
-
Actions
@@ -49,7 +46,7 @@ exports[`EuiEmptyPrompt props actions renders alone 1`] = ` class="euiEmptyPrompt" >
actions
@@ -60,7 +57,7 @@ exports[`EuiEmptyPrompt props actions renders an array 1`] = ` class="euiEmptyPrompt" >
`; +exports[`EuiEmptyPrompt props icon renders alone 1`] = ` +
+ + Custom icon + +
+
+`; + exports[`EuiEmptyPrompt props iconType renders alone 1`] = `
`; -exports[`EuiEmptyPrompt props title renders alone 1`] = ` +exports[`EuiEmptyPrompt props iconType renders with iconColor 1`] = `
+
+
+`; + +exports[`EuiEmptyPrompt props title renders alone 1`] = ` +
+
-
- title -
-
- + title +
`; diff --git a/src/components/empty_prompt/empty_prompt.test.tsx b/src/components/empty_prompt/empty_prompt.test.tsx index 1e6139bdcad..971233e9d7f 100644 --- a/src/components/empty_prompt/empty_prompt.test.tsx +++ b/src/components/empty_prompt/empty_prompt.test.tsx @@ -44,6 +44,22 @@ describe('EuiEmptyPrompt', () => { const component = render(); expect(component).toMatchSnapshot(); }); + + test('renders with iconColor', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + }); + + describe('icon', () => { + test('renders alone', () => { + const component = render( + Custom icon} /> + ); + expect(component).toMatchSnapshot(); + }); }); describe('title', () => { diff --git a/src/components/empty_prompt/empty_prompt.tsx b/src/components/empty_prompt/empty_prompt.tsx index 5749df2049f..5143591f510 100644 --- a/src/components/empty_prompt/empty_prompt.tsx +++ b/src/components/empty_prompt/empty_prompt.tsx @@ -19,7 +19,6 @@ import React, { FunctionComponent, - Fragment, HTMLAttributes, ReactElement, ReactNode, @@ -35,15 +34,41 @@ import { EuiText, EuiTextColor } from '../text'; export type EuiEmptyPromptProps = CommonProps & Omit, 'title'> & { + /* + * Accepts any `EuiIcon.type` or pass a custom node + */ iconType?: IconType; + /** + * Color for `iconType` when passed as an `IconType` + */ iconColor?: IconColor; + /** + * Custom icon replacing the one generated by `iconType` + */ + icon?: ReactNode; + /** + * Requires passing a single element that gets wrapped in an EuiTitle. + * Recommendation is a heading, preferrably an `

` if in its own section + */ title?: ReactElement; + /** + * Choose from one of the `EuiTitle.size` options + */ titleSize?: EuiTitleSize; + /** + * Gets wrapped in a subdued EuiText block. + * Recommendation is to pass typical text elements like `

` + */ body?: ReactNode; + /** + * Pass a single or an array of actions (buttons) that get stacked at the bottom. + * Recommendation is to pass the primary action first and secondary actions as empty buttons + */ actions?: ReactNode; }; export const EuiEmptyPrompt: FunctionComponent = ({ + icon, iconType, iconColor = 'subdued', title, @@ -55,51 +80,41 @@ export const EuiEmptyPrompt: FunctionComponent = ({ }) => { const classes = classNames('euiEmptyPrompt', className); - let icon; - - if (iconType) { - icon = ( - + let iconNode; + if (icon) { + iconNode = ( + <> + {icon} + + + ); + } else if (iconType) { + iconNode = ( + <> - - + + ); } - let content; - + let titleNode; + let bodyNode; if (body || title) { - let titleEl; - if (title) { - titleEl = ( - - {title} - - - ); + titleNode = {title}; } - let bodyEl; - if (body) { - bodyEl = ( - + bodyNode = ( + + {title && } {body} - + ); } - - content = ( - - {titleEl} - {bodyEl} - - ); } - let actionsEl; - + let actionsNode; if (actions) { let actionsRow; @@ -121,20 +136,20 @@ export const EuiEmptyPrompt: FunctionComponent = ({ actionsRow = actions; } - actionsEl = ( - - + actionsNode = ( + <> + {actionsRow} - + ); } return (

- {icon} - {content} - {body && actions && } - {actionsEl} + {iconNode} + {titleNode} + {bodyNode} + {actionsNode}
); }; diff --git a/src/components/index.js b/src/components/index.js index e93138a589f..b433fb8646a 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -206,6 +206,7 @@ export { EuiLoadingChart, EuiLoadingContent, EuiLoadingSpinner, + EuiLoadingLogo, } from './loading'; export { EuiKeyPadMenu, EuiKeyPadMenuItem } from './key_pad_menu'; diff --git a/src/components/loading/__snapshots__/loading_logo.test.tsx.snap b/src/components/loading/__snapshots__/loading_logo.test.tsx.snap new file mode 100644 index 00000000000..f8164c3131a --- /dev/null +++ b/src/components/loading/__snapshots__/loading_logo.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiLoadingLogo is rendered 1`] = ` +