diff --git a/dev_docs/assets/kibana_template_solution_nav.png b/dev_docs/assets/kibana_template_solution_nav.png new file mode 100644 index 0000000000000..4ffec1990ed0a Binary files /dev/null and b/dev_docs/assets/kibana_template_solution_nav.png differ diff --git a/dev_docs/assets/kibana_template_solution_nav_mobile.png b/dev_docs/assets/kibana_template_solution_nav_mobile.png new file mode 100644 index 0000000000000..487114f7a344a Binary files /dev/null and b/dev_docs/assets/kibana_template_solution_nav_mobile.png differ diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index aa38890a8ac9e..d9605ac5643ba 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -9,13 +9,13 @@ tags: ['kibana', 'dev', 'ui', 'tutorials'] `KibanaPageTemplate` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. -Refer to EUI's documentation on [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. +Refer to EUI's documentation on [**EuiPageTemplate**](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. ## `isEmptyState` Use the `isEmptyState` prop for when there is no page content to show. For example, before the user has created something, when no search results are found, before data is populated, or when permissions aren't met. -The default empty state uses any `pageHeader` info provided to populate an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. +The default empty state uses any `pageHeader` info provided to populate an [**EuiEmptyPrompt**](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. ```tsx + {...} + +``` + + +![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) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7f2d0768a1fb9..a585a0fc7542f 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -9,7 +9,7 @@ pageLoadAssetSize: charts: 195358 cloud: 21076 console: 46091 - core: 414000 + core: 432925 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 936b41e7682bb..3a748f3ceb6fd 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -49,6 +49,13 @@ top: $headerHeight; height: calc(100% - #{$headerHeight}); } + + @include euiBreakpoint('m', 'l', 'xl') { + .euiPageSideBar--sticky { + max-height: calc(100vh - #{$headerHeight}); + top: #{$headerHeight}; + } + } } .kbnBody { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 3b2feeecabb7c..2f54bd1d818b5 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -276,7 +276,7 @@ kibana_vars=( xpack.reporting.roles.allow xpack.reporting.roles.enabled xpack.rollup.enabled - xpack.ruleRegistry.unsafe.write.enabled + xpack.ruleRegistry.write.enabled xpack.searchprofiler.enabled xpack.security.audit.enabled xpack.security.audit.appender.type 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 89fa05615a039..a80e3a67fb2db 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 custom empty prompt only 1`] = ` @@ -33,6 +45,7 @@ exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` @@ -58,6 +76,12 @@ exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` exports[`KibanaPageTemplate render default empty prompt 1`] = ` @@ -72,7 +96,76 @@ exports[`KibanaPageTemplate render default empty prompt 1`] = ` test

} + iconColor="" iconType="test" />
`; + +exports[`KibanaPageTemplate render solutionNav 1`] = ` + + } + pageSideBarProps={ + Object { + "className": "kbnPageTemplate__pageSideBar", + } + } + restrictWidth={true} +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/page_template.scss b/src/plugins/kibana_react/public/page_template/page_template.scss new file mode 100644 index 0000000000000..4b8513311114d --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/page_template.scss @@ -0,0 +1,15 @@ +$euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); + +.kbnPageTemplate__pageSideBar { + padding: $euiSizeL; + background: + linear-gradient(160deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSizeXL, rgba(#FFF, 0) 0), + linear-gradient(175deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSize, rgba(#FFF, 0) 0); +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplate__pageSideBar { + width: auto; + padding: 0; + } +} 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 2ad9a81e7916c..43f96a6c2b98c 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 @@ -10,6 +10,46 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KibanaPageTemplate } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const navItems: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; describe('KibanaPageTemplate', () => { test('render default empty prompt', () => { @@ -66,4 +106,23 @@ describe('KibanaPageTemplate', () => { ); expect(component).toMatchSnapshot(); }); + + test('render solutionNav', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/kibana_react/public/page_template/page_template.tsx b/src/plugins/kibana_react/public/page_template/page_template.tsx index eb834d00402ef..0bbf97ca6ddb5 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.tsx @@ -5,10 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import './page_template.scss'; -import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; + +import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNav, + KibanaPageTemplateSolutionNavProps, +} from './solution_nav/solution_nav'; +/** + * A thin wrapper around EuiPageTemplate with a few Kibana specific additions + */ export type KibanaPageTemplateProps = EuiPageTemplateProps & { /** * Changes the template type depending on other props provided. @@ -17,6 +28,10 @@ export type KibanaPageTemplateProps = EuiPageTemplateProps & { * With `pageHeader` and `children`: Uses `centeredContent` */ isEmptyState?: boolean; + /** + * Quick creation of EuiSideNav. Hooks up mobile instance too + */ + solutionNav?: KibanaPageTemplateSolutionNavProps; }; export const KibanaPageTemplate: FunctionComponent = ({ @@ -27,6 +42,8 @@ export const KibanaPageTemplate: FunctionComponent = ({ restrictWidth = true, bottomBar, bottomBarProps, + pageSideBar, + solutionNav, ...rest }) => { // Needed for differentiating between union types @@ -38,6 +55,13 @@ export const KibanaPageTemplate: FunctionComponent = ({ }; } + /** + * Create the solution nav component + */ + if (solutionNav) { + pageSideBar = ; + } + /** * An easy way to create the right content for empty pages */ @@ -48,6 +72,7 @@ export const KibanaPageTemplate: FunctionComponent = ({ children = ( {pageTitle} : undefined} body={description ?

{description}

: undefined} actions={rightSideItems} @@ -62,8 +87,14 @@ export const KibanaPageTemplate: FunctionComponent = ({ return ( diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap new file mode 100644 index 0000000000000..0267357709534 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNav accepts EuiSideNavProps 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` +
+ +

+ + + Solution + +

+
+ + + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap new file mode 100644 index 0000000000000..ede09c5652c31 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNavAvatar renders 1`] = ` + +`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/index.ts b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts new file mode 100644 index 0000000000000..abbcde9a08486 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; +export { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss new file mode 100644 index 0000000000000..bdb88b2ab7baa --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss @@ -0,0 +1,22 @@ +.kbnPageTemplateSolutionNav__title { + margin-bottom: $euiSizeL; +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplateSolutionNav { + // TODO: Fix in EUI + .euiSideNav__mobileToggle { + height: auto; + font-size: $euiFontSizeM; + + .euiButtonEmpty__text { + overflow: visible; + } + } + } + + // Rely on the `mobileToggle` of the EuiSideNav component to title the navigation list + .kbnPageTemplateSolutionNav__title { + display: none; + } +} diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx new file mode 100644 index 0000000000000..1ba6cc924cda1 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const items: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; + +describe('KibanaPageTemplateSolutionNav', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders with icon', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + test('accepts EuiSideNavProps', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx new file mode 100644 index 0000000000000..4aa456f716dbd --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx @@ -0,0 +1,103 @@ +/* + * 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 './solution_nav.scss'; + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiTitle, EuiSideNav, EuiSideNavProps, htmlIdGenerator } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; + +export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { + /** + * Name of the solution, i.e. "Observability" + */ + name: KibanaPageTemplateSolutionNavAvatarProps['name']; + /** + * Solution logo, i.e. "logoObservability" + */ + icon?: KibanaPageTemplateSolutionNavAvatarProps['iconType']; +}; + +/** + * A wrapper around EuiSideNav but also creates the appropriate title with optional solution logo + */ +export const KibanaPageTemplateSolutionNav: FunctionComponent = ({ + name, + icon, + items, + ...rest +}) => { + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + /** + * Create the avatar. + */ + let solutionAvatar; + if (icon) { + solutionAvatar = ; + } + + /** + * Create the required title. + * a11y: Since the heading can't be nested inside `