diff --git a/packages/kbn-shared-ux-components/src/index.ts b/packages/kbn-shared-ux-components/src/index.ts
index 557ac980a14c6..c374bd91f023c 100644
--- a/packages/kbn-shared-ux-components/src/index.ts
+++ b/packages/kbn-shared-ux-components/src/index.ts
@@ -95,6 +95,23 @@ export const LazyIconButtonGroup = React.lazy(() =>
*/
export const IconButtonGroup = withSuspense(LazyIconButtonGroup);
+/**
+ * The lazily loaded `KibanaPageTemplate` component that is wrapped by the `withSuspense` HOC. Consumers should use
+ * `React.Suspense` or `withSuspense` HOC to load this component.
+ */
+export const KibanaPageTemplateLazy = React.lazy(() =>
+ import('./page_template').then(({ KibanaPageTemplate }) => ({
+ default: KibanaPageTemplate,
+ }))
+);
+
+/**
+ * A `KibanaPageTemplate` component that is wrapped by the `withSuspense` HOC. This component can
+ * be used directly by consumers and will load the `KibanaPageTemplateLazy` component lazily with
+ * a predefined fallback and error boundary.
+ */
+export const KibanaPageTemplate = withSuspense(KibanaPageTemplateLazy);
+
/**
* The lazily loaded `KibanaPageTemplateSolutionNav` component that is wrapped by the `withSuspense` HOC. Consumers should use
* `React.Suspense` or `withSuspense` HOC to load this component.
diff --git a/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template.test.tsx.snap
new file mode 100644
index 0000000000000..e41292f549c99
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template.test.tsx.snap
@@ -0,0 +1,197 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KibanaPageTemplate render basic template 1`] = `
+
+`;
+
+exports[`KibanaPageTemplate render noDataConfig && solutionNav 1`] = `
+
+`;
+
+exports[`KibanaPageTemplate render noDataConfig 1`] = `
+
+`;
+
+exports[`KibanaPageTemplate render solutionNav 1`] = `
+
+
+ Child element
+
+
+`;
diff --git a/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template_inner.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template_inner.test.tsx.snap
new file mode 100644
index 0000000000000..ef665dff6fe6d
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template_inner.test.tsx.snap
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KibanaPageTemplateInner custom template 1`] = `
+
+
+ test
+
+ }
+ iconColor=""
+ iconType="test"
+ title={
+
+ test
+
+ }
+ />
+
+`;
+
+exports[`KibanaPageTemplateInner isEmpty no pageHeader 1`] = `
+
+`;
+
+exports[`KibanaPageTemplateInner isEmpty pageHeader & children 1`] = `
+
+
+ Child element
+
+
+`;
+
+exports[`KibanaPageTemplateInner isEmpty pageHeader & no children 1`] = `
+
+
+ test
+
+ }
+ iconColor=""
+ iconType="test"
+ title={
+
+ test
+
+ }
+ />
+
+`;
diff --git a/packages/kbn-shared-ux-components/src/page_template/__snapshots__/with_solution_nav.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/with_solution_nav.test.tsx.snap
new file mode 100644
index 0000000000000..0064b0a638cd2
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/__snapshots__/with_solution_nav.test.tsx.snap
@@ -0,0 +1,125 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WithSolutionNav renders wrapped component 1`] = `
+
+ }
+ pageSideBarProps={
+ Object {
+ "className": "kbnPageTemplate__pageSideBar",
+ "paddingSize": "none",
+ }
+ }
+/>
+`;
+
+exports[`WithSolutionNav with children 1`] = `
+
+ }
+ pageSideBarProps={
+ Object {
+ "className": "kbnPageTemplate__pageSideBar",
+ "paddingSize": "none",
+ }
+ }
+>
+
+ Child component
+
+
+`;
diff --git a/packages/kbn-shared-ux-components/src/page_template/assets/kibana_template_no_data_config.png b/packages/kbn-shared-ux-components/src/page_template/assets/kibana_template_no_data_config.png
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/packages/kbn-shared-ux-components/src/page_template/index.ts b/packages/kbn-shared-ux-components/src/page_template/index.ts
new file mode 100644
index 0000000000000..caed703e5d656
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/index.ts
@@ -0,0 +1,12 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { NoDataCard, ElasticAgentCard } from './no_data_page';
+export { NoDataPage } from './no_data_page';
+export { KibanaPageTemplate } from './page_template';
+export type { KibanaPageTemplateProps } from './types';
diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/index.ts b/packages/kbn-shared-ux-components/src/page_template/no_data_page/index.ts
index c1b0ac2e13395..894097727cd1f 100644
--- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/index.ts
+++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/index.ts
@@ -9,3 +9,4 @@
export { NoDataCard, ElasticAgentCard } from './no_data_card';
export { NoDataPage } from './no_data_page';
export type { NoDataPageProps } from './types';
+export { NoDataConfigPage, NoDataConfigPageWithSolutionNavBar } from './no_data_config_page';
diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/__snapshots__/no_data_config_page.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/__snapshots__/no_data_config_page.test.tsx.snap
new file mode 100644
index 0000000000000..047f44e0d319c
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/__snapshots__/no_data_config_page.test.tsx.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NoDataConfigPage renders 1`] = `
+
+
+
+`;
diff --git a/packages/kbn-shared-ux-components/src/page_template/index.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/index.tsx
similarity index 76%
rename from packages/kbn-shared-ux-components/src/page_template/index.tsx
rename to packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/index.tsx
index d469a2fb34c10..0bdde40021398 100644
--- a/packages/kbn-shared-ux-components/src/page_template/index.tsx
+++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/index.tsx
@@ -6,5 +6,4 @@
* Side Public License, v 1.
*/
-export { NoDataCard, ElasticAgentCard } from './no_data_page';
-export { NoDataPage } from './no_data_page';
+export { NoDataConfigPage, NoDataConfigPageWithSolutionNavBar } from './no_data_config_page';
diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.test.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.test.tsx
new file mode 100644
index 0000000000000..dc618a068e120
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.test.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { NoDataConfigPage } from './no_data_config_page';
+
+describe('NoDataConfigPage', () => {
+ const noDataConfig = {
+ solution: 'Kibana',
+ logo: 'logoKibana',
+ docsLink: 'test-link',
+ action: {
+ kibana: {
+ button: 'Click me',
+ onClick: jest.fn(),
+ description: 'Page with no data',
+ },
+ },
+ };
+ test('renders', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx
new file mode 100644
index 0000000000000..77c2d659b56ef
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { EuiPageTemplate } from '@elastic/eui';
+import React from 'react';
+import { NoDataPage } from '../no_data_page';
+import { withSolutionNav } from '../../with_solution_nav';
+import { KibanaPageTemplateProps } from '../../types';
+import { getClasses, NO_DATA_PAGE_TEMPLATE_PROPS } from '../../util';
+
+export const NoDataConfigPage = (props: KibanaPageTemplateProps) => {
+ const { className, noDataConfig, ...rest } = props;
+
+ if (!noDataConfig) {
+ return null;
+ }
+
+ const template = NO_DATA_PAGE_TEMPLATE_PROPS.template;
+ const classes = getClasses(template, className);
+
+ return (
+
+
+
+ );
+};
+
+export const NoDataConfigPageWithSolutionNavBar = withSolutionNav(NoDataConfigPage);
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template.mdx b/packages/kbn-shared-ux-components/src/page_template/page_template.mdx
new file mode 100644
index 0000000000000..59acf8910cf29
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template.mdx
@@ -0,0 +1,168 @@
+---
+id: sharedUX/Components/PageTemplate
+slug: /shared-ux-components/page_template/page_template
+title: Page Template
+summary: A Kibana-specific wrapper around `EuiTemplate`
+tags: ['shared-ux', 'component']
+date: 2022-04-04
+---
+
+`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.
+
+## `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.
+
+```tsx
+
+ Create new dashboard
+ ,
+ ],
+ }}
+/>
+```
+
+
+
+
+ Because all properties of the page header are optional, the empty state has the potential to
+ render blank. Make sure your empty state doesn't leave the user confused.
+
+
+
+### Custom empty state
+
+You can also provide a custom empty prompt to replace the pre-built one. You'll want to remove any `pageHeader` props and pass an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) directly as the child of KibanaPageTemplate.
+
+```tsx
+
+ No data}
+ body="You have no data. Would you like some of ours?"
+ actions={[
+
+ Get sample data
+ ,
+ ]}
+ />
+
+```
+
+
+
+### Empty states with a page header
+
+When passing both a `pageHeader` configuration and `isEmptyState`, the component will render the proper template (`centeredContent`). Be sure to reduce the heading level within your child empty prompt to ``.
+
+```tsx
+
+ No data
}
+ body="You have no data. Would you like some of ours?"
+ actions={[
+
+ Get sample data
+ ,
+ ]}
+ />
+
+```
+
+
+
+## `solutionNav`
+
+To add left side navigation for your solution, we recommend passing [**EuiSideNav**](https://elastic.github.io/eui/#/navigation/side-nav) props to the `solutionNav` prop. The template component will then handle the mobile views and add the solution nav embellishments. On top of the EUI props, you'll need to pass your solution `name` and an optional `icon`.
+
+If you need to custom side bar content, you will need to pass you own navigation component to `pageSideBar`. We still recommend using [**EuiSideNav**](https://elastic.github.io/eui/#/navigation/side-nav).
+
+When using `EuiSideNav`, root level items should not be linked but provide section labelling only.
+
+```tsx
+
+ {...}
+
+```
+
+
+
+
+
+## `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 [`NoDataPageProps`](https://github.com/elastic/kibana/blob/main/packages/kbn-shared-ux-components/src/page_template/no_data_page/types.ts#L14):
+
+1. `solution: string`: Single name for the current solution, used to auto-generate the title, logo, and description *(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. `action: Record`: An object of `NoDataPageActions` configurations with a unique primary key *(required)*
+
+### `NoDataPageActions`
+
+There is a main action for adding data that we promote throughout Kibana - Elastic Agent. It is added to the card by using the key `elasticAgent`. For consistent messaging, this card is pre-configured but requires specific `href`s and/or `onClick` handlers for directing the user to the right location for that solution.
+
+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: 'Analytics',
+ logo: 'logoKibana',
+ docsLink: '#',
+ action: {
+ elasticAgent: {
+ href: '#',
+ },
+ },
+};
+
+// Conditionally apply the configuration if there is no data
+
+ {/* Children will be ignored */}
+
+```
+
+
\ No newline at end of file
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template.scss b/packages/kbn-shared-ux-components/src/page_template/page_template.scss
new file mode 100644
index 0000000000000..aec93da6217ee
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template.scss
@@ -0,0 +1,19 @@
+.kbnPageTemplate__pageSideBar {
+ overflow: hidden;
+ // Temporary hack till the sizing is changed directly in EUI
+ min-width: 248px;
+
+ @include euiCanAnimate {
+ transition: min-width $euiAnimSpeedFast $euiAnimSlightResistance;
+ }
+
+ &.kbnPageTemplate__pageSideBar--shrink {
+ min-width: $euiSizeXXL;
+ }
+
+ .kbnPageTemplate--centeredBody & {
+ @include euiBreakpoint('m', 'l', 'xl') {
+ border-right: $euiBorderThin;
+ }
+ }
+}
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template.stories.tsx b/packages/kbn-shared-ux-components/src/page_template/page_template.stories.tsx
new file mode 100644
index 0000000000000..d840e459389b2
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template.stories.tsx
@@ -0,0 +1,143 @@
+/*
+ * 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 { EuiButton, EuiText } from '@elastic/eui';
+import { KibanaPageTemplate } from './page_template';
+import mdx from './page_template.mdx';
+import { KibanaPageTemplateSolutionNavProps } from './solution_nav';
+import { KibanaPageTemplateProps } from './types';
+
+export default {
+ title: 'Page Template/Page Template',
+ description:
+ 'A thin wrapper around `EuiTemplate`. Takes care of styling, empty state and no data config',
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
+};
+
+type Params = Pick;
+
+const noDataConfig = {
+ solution: 'Kibana',
+ action: {
+ elasticAgent: {},
+ },
+ docsLink: 'http://wwww.docs.elastic.co',
+};
+
+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',
+ },
+ ],
+ },
+];
+
+const solutionNavBar = {
+ items,
+ logo: 'logoKibana',
+ name: 'Kibana',
+ action: { elasticAgent: {} },
+};
+
+const content = (
+
+
+ Page Content goes here
+
+
+);
+
+const header = {
+ iconType: 'logoKibana',
+ pageTitle: 'Kibana',
+ description: 'Welcome to Kibana!',
+ rightSideItems: [Add something, Do something],
+};
+
+export const WithNoDataConfig = () => {
+ return ;
+};
+
+export const WithNoDataConfigAndSolutionNav = () => {
+ return ;
+};
+
+export const PureComponent = (params: Params) => {
+ return (
+
+ {content}
+
+ );
+};
+
+PureComponent.argTypes = {
+ isEmptyState: {
+ control: 'boolean',
+ defaultValue: false,
+ },
+ pageHeader: {
+ control: 'boolean',
+ defaultValue: true,
+ },
+ solutionNav: {
+ control: 'boolean',
+ defaultValue: true,
+ },
+};
+
+PureComponent.parameters = {
+ layout: 'fullscreen',
+};
+
+WithNoDataConfig.parameters = {
+ layout: 'fullscreen',
+};
+
+WithNoDataConfigAndSolutionNav.parameters = {
+ layout: 'fullscreen',
+};
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template.test.tsx b/packages/kbn-shared-ux-components/src/page_template/page_template.test.tsx
new file mode 100644
index 0000000000000..8d073e14f7776
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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, render } from 'enzyme';
+import { KibanaPageTemplate } from './page_template';
+import { KibanaPageTemplateSolutionNavProps } from './solution_nav';
+import { NoDataPageProps } from './no_data_page';
+
+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',
+ },
+ ],
+ },
+];
+
+const solutionNav = {
+ name: 'Kibana',
+ icon: 'logoKibana',
+ items,
+};
+
+const noDataConfig: NoDataPageProps = {
+ solution: 'Elastic',
+ action: {
+ elasticAgent: {},
+ },
+ docsLink: 'test',
+};
+
+describe('KibanaPageTemplate', () => {
+ test('render noDataConfig && solutionNav', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ test('render noDataConfig', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+ });
+
+ test('render solutionNav', () => {
+ const component = shallow(
+
+ Child element
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ test('render basic template', () => {
+ const component = render(
+
+ Child element
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template.tsx b/packages/kbn-shared-ux-components/src/page_template/page_template.tsx
new file mode 100644
index 0000000000000..6d63d54e9b9dd
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template.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.
+ */
+
+import './page_template.scss';
+
+import React, { FunctionComponent } from 'react';
+
+import { NoDataConfigPage, NoDataConfigPageWithSolutionNavBar } from './no_data_page';
+import { KibanaPageTemplateInner, KibanaPageTemplateWithSolutionNav } from './page_template_inner';
+import { KibanaPageTemplateProps } from './types';
+
+export const KibanaPageTemplate: FunctionComponent = ({
+ template,
+ className,
+ children,
+ solutionNav,
+ noDataConfig,
+ ...rest
+}) => {
+ /**
+ * If passing the custom template of `noDataConfig`
+ */
+ if (noDataConfig && solutionNav) {
+ return (
+
+ );
+ }
+
+ if (noDataConfig) {
+ return (
+
+ );
+ }
+
+ if (solutionNav) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template_inner.test.tsx b/packages/kbn-shared-ux-components/src/page_template/page_template_inner.test.tsx
new file mode 100644
index 0000000000000..c17b83c4f4eed
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template_inner.test.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+// imports from npm packages
+import React from 'react';
+import { shallow } from 'enzyme';
+
+// imports from elastic packages
+import { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui';
+
+// imports from immediate files
+import { KibanaPageTemplateInner } from './page_template_inner';
+
+describe('KibanaPageTemplateInner', () => {
+ const pageHeader = {
+ iconType: 'test',
+ pageTitle: 'test',
+ description: 'test',
+ rightSideItems: ['test'],
+ };
+
+ describe('isEmpty', () => {
+ test('pageHeader & children', () => {
+ const component = shallow(
+ Child element}
+ />
+ );
+ expect(component).toMatchSnapshot();
+ expect(component.find('[data-test-subj="child"]').length).toBe(1);
+ });
+
+ test('pageHeader & no children', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ expect(component.find(EuiEmptyPrompt).length).toBe(1);
+ });
+
+ test('no pageHeader', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ test('custom template', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ expect(component.find(EuiPageTemplate).props().template).toEqual('centeredContent');
+ });
+});
diff --git a/packages/kbn-shared-ux-components/src/page_template/page_template_inner.tsx b/packages/kbn-shared-ux-components/src/page_template/page_template_inner.tsx
new file mode 100644
index 0000000000000..cef22f2713efc
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/page_template_inner.tsx
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { FunctionComponent } from 'react';
+
+import { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui';
+
+import { withSolutionNav } from './with_solution_nav';
+import { KibanaPageTemplateProps } from './types';
+import { getClasses } from './util';
+
+type Props = KibanaPageTemplateProps;
+
+/**
+ * A thin wrapper around EuiPageTemplate with a few Kibana specific additions
+ */
+export const KibanaPageTemplateInner: FunctionComponent = ({
+ template,
+ className,
+ pageHeader,
+ children,
+ isEmptyState,
+ ...rest
+}) => {
+ /**
+ * An easy way to create the right content for empty pages
+ */
+ const emptyStateDefaultTemplate = 'centeredBody';
+ let header = pageHeader;
+
+ if (isEmptyState) {
+ if (pageHeader && !children) {
+ template = template ?? emptyStateDefaultTemplate;
+ const { iconType, pageTitle, description, rightSideItems } = pageHeader;
+ const title = pageTitle ? {pageTitle}
: undefined;
+ const body = description ? {description}
: undefined;
+ header = undefined;
+ children = (
+
+ );
+ } else if (pageHeader && children) {
+ template = template ?? 'centeredContent';
+ } else if (!pageHeader) {
+ template = template ?? emptyStateDefaultTemplate;
+ }
+ }
+
+ const classes = getClasses(template, className);
+ return (
+
+ {children}
+
+ );
+};
+
+export const KibanaPageTemplateWithSolutionNav = withSolutionNav(KibanaPageTemplateInner);
diff --git a/packages/kbn-shared-ux-components/src/page_template/types.ts b/packages/kbn-shared-ux-components/src/page_template/types.ts
new file mode 100644
index 0000000000000..cd4764a976db8
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/types.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { EuiPageTemplateProps } from '@elastic/eui';
+import { KibanaPageTemplateSolutionNavProps } from './solution_nav';
+import { NoDataPageProps } from './no_data_page';
+
+export type KibanaPageTemplateProps = EuiPageTemplateProps & {
+ /**
+ * Changes the template type depending on other props provided.
+ * With `pageHeader` only: Uses `centeredBody` and fills an EuiEmptyPrompt with `pageHeader` info.
+ * With `children` only: Uses `centeredBody`
+ * With `pageHeader` and `children`: Uses `centeredContent`
+ */
+ isEmptyState?: boolean;
+ /**
+ * Quick creation of EuiSideNav. Hooks up mobile instance too
+ */
+ solutionNav?: KibanaPageTemplateSolutionNavProps;
+ /**
+ * Accepts a configuration object, that when provided, ignores pageHeader and children and instead
+ * displays Agent, Beats, and custom cards to direct users to the right ingest location
+ */
+ noDataConfig?: NoDataPageProps;
+};
diff --git a/packages/kbn-shared-ux-components/src/page_template/util/constants.ts b/packages/kbn-shared-ux-components/src/page_template/util/constants.ts
new file mode 100644
index 0000000000000..92dbe1cb16279
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/util/constants.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { KibanaPageTemplateProps } from '../types';
+
+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',
+ paddingSize: 'none',
+ },
+};
diff --git a/packages/kbn-shared-ux-components/src/page_template/util/index.ts b/packages/kbn-shared-ux-components/src/page_template/util/index.ts
new file mode 100644
index 0000000000000..adfefdf834566
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/util/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 { getClasses } from './presentation';
+export * from './constants';
diff --git a/packages/kbn-shared-ux-components/src/page_template/util/presentation.ts b/packages/kbn-shared-ux-components/src/page_template/util/presentation.ts
new file mode 100644
index 0000000000000..ab7144ee37b57
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/util/presentation.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.
+ */
+
+import classNames from 'classnames';
+
+export const getClasses = (template: string | undefined, className: string | undefined) => {
+ return classNames('kbnPageTemplate', { [`kbnPageTemplate--${template}`]: template }, className);
+};
diff --git a/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.test.tsx b/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.test.tsx
new file mode 100644
index 0000000000000..0d0ac4cf71bfc
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.test.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 { withSolutionNav } from './with_solution_nav';
+import { KibanaPageTemplateSolutionNavProps } from './solution_nav';
+
+const TestComponent = () => {
+ return This is a wrapped component
;
+};
+
+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',
+ },
+ ],
+ },
+];
+
+const solutionNav = {
+ name: 'Kibana',
+ icon: 'logoKibana',
+ items,
+};
+
+describe('WithSolutionNav', () => {
+ test('renders wrapped component', () => {
+ const WithSolutionNavTestComponent = withSolutionNav(TestComponent);
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+ });
+
+ test('with children', () => {
+ const WithSolutionNavTestComponent = withSolutionNav(TestComponent);
+ const component = shallow(
+
+ Child component
+
+ );
+ expect(component).toMatchSnapshot();
+ expect(component.find('.child').html()).toContain('Child component');
+ });
+});
diff --git a/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.tsx b/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.tsx
new file mode 100644
index 0000000000000..07d78dc87f40b
--- /dev/null
+++ b/packages/kbn-shared-ux-components/src/page_template/with_solution_nav.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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, { ComponentType, useState } from 'react';
+import classNames from 'classnames';
+import { useIsWithinBreakpoints } from '@elastic/eui';
+import { EuiPageSideBarProps } from '@elastic/eui/src/components/page/page_side_bar';
+import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav';
+import { KibanaPageTemplateProps } from './types';
+
+// https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging
+function getDisplayName(Component: ComponentType) {
+ return Component.displayName || Component.name || 'UnnamedComponent';
+}
+
+type SolutionNavProps = KibanaPageTemplateProps & {
+ solutionNav: KibanaPageTemplateSolutionNavProps;
+};
+
+const SOLUTION_NAV_COLLAPSED_KEY = 'solutionNavIsCollapsed';
+
+export const withSolutionNav = (WrappedComponent: ComponentType) => {
+ const WithSolutionNav = (props: SolutionNavProps) => {
+ const isMediumBreakpoint = useIsWithinBreakpoints(['m']);
+ const isLargerBreakpoint = useIsWithinBreakpoints(['l', 'xl']);
+ const [isSideNavOpenOnDesktop, setisSideNavOpenOnDesktop] = useState(
+ !JSON.parse(String(localStorage.getItem(SOLUTION_NAV_COLLAPSED_KEY)))
+ );
+ const { solutionNav, ...propagatedProps } = props;
+ const { children, isEmptyState, template } = propagatedProps;
+ const toggleOpenOnDesktop = () => {
+ setisSideNavOpenOnDesktop(!isSideNavOpenOnDesktop);
+ // Have to store it as the opposite of the default we want
+ localStorage.setItem(SOLUTION_NAV_COLLAPSED_KEY, JSON.stringify(isSideNavOpenOnDesktop));
+ };
+ const sideBarClasses = classNames(
+ 'kbnPageTemplate__pageSideBar',
+ {
+ 'kbnPageTemplate__pageSideBar--shrink':
+ isMediumBreakpoint || (isLargerBreakpoint && !isSideNavOpenOnDesktop),
+ },
+ props.pageSideBarProps?.className
+ );
+
+ const templateToUse = isEmptyState && !template ? 'centeredContent' : template;
+
+ const pageSideBar = (
+
+ );
+ const pageSideBarProps = {
+ paddingSize: 'none',
+ ...props.pageSideBarProps,
+ className: sideBarClasses,
+ } as EuiPageSideBarProps; // needed because for some reason 'none' is not recognized as a valid value for paddingSize
+ return (
+
+ {children}
+
+ );
+ };
+
+ WithSolutionNav.displayName = `WithSolutionNavBar(${getDisplayName(WrappedComponent)})`;
+
+ return WithSolutionNav;
+};