# Conflicts: # packages/kbn-optimizer/limits.yml
This commit is contained in:
parent
4593c32dd2
commit
0ec9720a63
BIN
dev_docs/assets/kibana_template_no_data_config.png
Normal file
BIN
dev_docs/assets/kibana_template_no_data_config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 512 KiB |
|
@ -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: '#',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
<KibanaPageTemplate
|
||||
solutionNav={/* Solution navigation still show if it exists and use the right template type */}
|
||||
pageHeader={/* Page header will be ignored */}
|
||||
noDataConfig={hasData ? undefined : noDataConfig}
|
||||
>
|
||||
{/* Children will be ignored */}
|
||||
</KibanaPageTemplate>
|
||||
```
|
||||
|
||||
|
||||
![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)
|
||||
|
|
|
@ -38,11 +38,11 @@ pageLoadAssetSize:
|
|||
infra: 184320
|
||||
fleet: 465774
|
||||
ingestPipelines: 58003
|
||||
inputControlVis: 172819
|
||||
inspector: 148999
|
||||
kibanaLegacy: 107855
|
||||
kibanaOverview: 56426
|
||||
kibanaReact: 162353
|
||||
inputControlVis: 172675
|
||||
inspector: 148711
|
||||
kibanaLegacy: 107711
|
||||
kibanaOverview: 56279
|
||||
kibanaReact: 188705
|
||||
kibanaUtils: 198829
|
||||
lens: 96624
|
||||
licenseManagement: 41817
|
||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 93 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.5 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.5 KiB |
1
src/plugins/kibana_react/public/assets/texture.svg
Normal file
1
src/plugins/kibana_react/public/assets/texture.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 68 KiB |
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`KibanaPageTemplate render basic template 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate"
|
||||
pageHeader={
|
||||
Object {
|
||||
"description": "test",
|
||||
|
@ -24,6 +25,7 @@ exports[`KibanaPageTemplate render basic template 1`] = `
|
|||
|
||||
exports[`KibanaPageTemplate render custom empty prompt only 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate kbnPageTemplate--centeredBody"
|
||||
pageSideBarProps={
|
||||
Object {
|
||||
"className": "kbnPageTemplate__pageSideBar",
|
||||
|
@ -45,6 +47,7 @@ exports[`KibanaPageTemplate render custom empty prompt only 1`] = `
|
|||
|
||||
exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate kbnPageTemplate--centeredContent"
|
||||
pageHeader={
|
||||
Object {
|
||||
"description": "test",
|
||||
|
@ -76,6 +79,7 @@ exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = `
|
|||
|
||||
exports[`KibanaPageTemplate render default empty prompt 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate kbnPageTemplate--centeredBody"
|
||||
pageSideBarProps={
|
||||
Object {
|
||||
"className": "kbnPageTemplate__pageSideBar",
|
||||
|
@ -102,8 +106,89 @@ exports[`KibanaPageTemplate render default empty prompt 1`] = `
|
|||
</EuiPageTemplate>
|
||||
`;
|
||||
|
||||
exports[`KibanaPageTemplate render noDataContent 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate kbnPageTemplate--centeredBody"
|
||||
pageContentProps={
|
||||
Object {
|
||||
"color": "transparent",
|
||||
"hasShadow": false,
|
||||
}
|
||||
}
|
||||
pageSideBar={
|
||||
<KibanaPageTemplateSolutionNav
|
||||
icon="solution"
|
||||
isOpenOnDesktop={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "1.1",
|
||||
"name": "Ingest Node Pipelines",
|
||||
},
|
||||
Object {
|
||||
"id": "1.2",
|
||||
"name": "Logstash Pipelines",
|
||||
},
|
||||
Object {
|
||||
"id": "1.3",
|
||||
"name": "Beats Central Management",
|
||||
},
|
||||
],
|
||||
"name": "Ingest",
|
||||
},
|
||||
Object {
|
||||
"id": "2",
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "2.1",
|
||||
"name": "Index Management",
|
||||
},
|
||||
Object {
|
||||
"id": "2.2",
|
||||
"name": "Index Lifecycle Policies",
|
||||
},
|
||||
Object {
|
||||
"id": "2.3",
|
||||
"name": "Snapshot and Restore",
|
||||
},
|
||||
],
|
||||
"name": "Data",
|
||||
},
|
||||
]
|
||||
}
|
||||
name="Solution"
|
||||
onCollapse={[Function]}
|
||||
/>
|
||||
}
|
||||
pageSideBarProps={
|
||||
Object {
|
||||
"className": "kbnPageTemplate__pageSideBar",
|
||||
"paddingSize": "none",
|
||||
}
|
||||
}
|
||||
restrictWidth={950}
|
||||
template="centeredBody"
|
||||
>
|
||||
<NoDataPage
|
||||
actions={
|
||||
Object {
|
||||
"beats": Object {},
|
||||
"custom": Object {},
|
||||
"elasticAgent": Object {},
|
||||
}
|
||||
}
|
||||
docsLink="test"
|
||||
solution="Elastic"
|
||||
/>
|
||||
</EuiPageTemplate>
|
||||
`;
|
||||
|
||||
exports[`KibanaPageTemplate render solutionNav 1`] = `
|
||||
<EuiPageTemplate
|
||||
className="kbnPageTemplate"
|
||||
pageHeader={
|
||||
Object {
|
||||
"description": "test",
|
||||
|
|
|
@ -7,3 +7,5 @@
|
|||
*/
|
||||
|
||||
export { KibanaPageTemplate, KibanaPageTemplateProps } from './page_template';
|
||||
export { KibanaPageTemplateSolutionNavAvatar } from './solution_nav';
|
||||
export * from './no_data_page';
|
||||
|
|
117
src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap
generated
Normal file
117
src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NoDataPage render 1`] = `
|
||||
<div
|
||||
className="kbnNoDataPageContents"
|
||||
>
|
||||
<EuiText
|
||||
textAlign="center"
|
||||
>
|
||||
<KibanaPageTemplateSolutionNavAvatar
|
||||
iconType="logoElastic"
|
||||
name="Elastic"
|
||||
size="xxl"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Welcome to Elastic {solution}!"
|
||||
id="kibana-react.noDataPage.welcomeTitle"
|
||||
values={
|
||||
Object {
|
||||
"solution": "Elastic",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h1>
|
||||
<EuiTextColor
|
||||
color="subdued"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add your data to get started, or {link} about {solution}."
|
||||
id="kibana-react.noDataPage.intro"
|
||||
values={
|
||||
Object {
|
||||
"link": <EuiLink
|
||||
href="test"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="learn more"
|
||||
id="kibana-react.noDataPage.intro.link"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
"solution": "Elastic",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
/>
|
||||
<EuiFlexGrid
|
||||
columns={2}
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "space-around",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="kbnNoDataPageContents__item"
|
||||
key="empty-page-agent-action"
|
||||
>
|
||||
<ElasticAgentCard
|
||||
solution="Elastic"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="kbnNoDataPageContents__item"
|
||||
key="empty-page-beats-action"
|
||||
>
|
||||
<ElasticBeatsCard
|
||||
solution="Elastic"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="kbnNoDataPageContents__item"
|
||||
key="empty-page-custom-action"
|
||||
>
|
||||
<NoDataCard />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer
|
||||
size="xxl"
|
||||
/>
|
||||
<EuiText
|
||||
color="subdued"
|
||||
textAlign="center"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Confused on which to use? {link}"
|
||||
id="kibana-react.noDataPage.cantDecide"
|
||||
values={
|
||||
Object {
|
||||
"link": <EuiLink
|
||||
href="https://www.elastic.co/guide/en/fleet/current/beats-agent-comparison.html"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Check our docs for more information."
|
||||
id="kibana-react.noDataPage.cantDecide.link"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
`;
|
|
@ -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';
|
|
@ -0,0 +1,81 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ElasticAgentCard props button 1`] = `
|
||||
<EuiCard
|
||||
betaBadgeLabel="Recommended"
|
||||
description="The Elastic Agent provides a simple, unified way to
|
||||
collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/integrations/browse"
|
||||
>
|
||||
Button
|
||||
</EuiButton>
|
||||
}
|
||||
href="app/integrations/browse"
|
||||
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
|
||||
paddingSize="l"
|
||||
title="Add a Solution integration"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCard props href 1`] = `
|
||||
<EuiCard
|
||||
betaBadgeLabel="Recommended"
|
||||
description="The Elastic Agent provides a simple, unified way to
|
||||
collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="#"
|
||||
>
|
||||
Button
|
||||
</EuiButton>
|
||||
}
|
||||
href="#"
|
||||
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
|
||||
paddingSize="l"
|
||||
title="Add a Solution integration"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCard props recommended 1`] = `
|
||||
<EuiCard
|
||||
betaBadgeLabel="Recommended"
|
||||
description="The Elastic Agent provides a simple, unified way to
|
||||
collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/integrations/browse"
|
||||
>
|
||||
Find an integration for Solution
|
||||
</EuiButton>
|
||||
}
|
||||
href="app/integrations/browse"
|
||||
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
|
||||
paddingSize="l"
|
||||
title="Add a Solution integration"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCard renders 1`] = `
|
||||
<EuiCard
|
||||
betaBadgeLabel="Recommended"
|
||||
description="The Elastic Agent provides a simple, unified way to
|
||||
collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/integrations/browse"
|
||||
>
|
||||
Find an integration for Solution
|
||||
</EuiButton>
|
||||
}
|
||||
href="app/integrations/browse"
|
||||
image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
|
||||
paddingSize="l"
|
||||
title="Add a Solution integration"
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,78 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ElasticBeatsCard props button 1`] = `
|
||||
<EuiCard
|
||||
description="
|
||||
Beats send data from hundreds or thousands of machines and systems to Logstash or Elasticsearch."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/home#/tutorial"
|
||||
>
|
||||
Button
|
||||
</EuiButton>
|
||||
}
|
||||
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`] = `
|
||||
<EuiCard
|
||||
description="
|
||||
Beats send data from hundreds or thousands of machines and systems to Logstash or Elasticsearch."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="#"
|
||||
>
|
||||
Button
|
||||
</EuiButton>
|
||||
}
|
||||
href="#"
|
||||
image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
|
||||
paddingSize="l"
|
||||
title="Add data with Beats"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticBeatsCard props recommended 1`] = `
|
||||
<EuiCard
|
||||
betaBadgeLabel="Recommended"
|
||||
description="
|
||||
Beats send data from hundreds or thousands of machines and systems to Logstash or Elasticsearch."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/home#/tutorial"
|
||||
>
|
||||
Install Beats for Solution
|
||||
</EuiButton>
|
||||
}
|
||||
href="app/home#/tutorial"
|
||||
image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
|
||||
paddingSize="l"
|
||||
title="Add data with Beats"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticBeatsCard renders 1`] = `
|
||||
<EuiCard
|
||||
description="
|
||||
Beats send data from hundreds or thousands of machines and systems to Logstash or Elasticsearch."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
href="app/home#/tutorial"
|
||||
>
|
||||
Install Beats for Solution
|
||||
</EuiButton>
|
||||
}
|
||||
href="app/home#/tutorial"
|
||||
image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg"
|
||||
paddingSize="l"
|
||||
title="Add data with Beats"
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,156 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NoDataCard props button 1`] = `
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned"
|
||||
>
|
||||
<div
|
||||
class="euiCard__content"
|
||||
>
|
||||
<span
|
||||
class="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
Card title
|
||||
</span>
|
||||
<div
|
||||
class="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiCard__footer"
|
||||
>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Button
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NoDataCard props href 1`] = `
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable"
|
||||
>
|
||||
<div
|
||||
class="euiCard__content"
|
||||
>
|
||||
<span
|
||||
class="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
<a
|
||||
aria-describedby="generated-idDescription"
|
||||
class="euiCard__titleAnchor"
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Card title
|
||||
</a>
|
||||
</span>
|
||||
<div
|
||||
class="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiCard__footer"
|
||||
>
|
||||
<a
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
href="#"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Button
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NoDataCard props recommended 1`] = `
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned euiCard--hasBetaBadge"
|
||||
>
|
||||
<div
|
||||
class="euiCard__content"
|
||||
>
|
||||
<span
|
||||
class="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
Card title
|
||||
</span>
|
||||
<div
|
||||
class="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="euiCard__betaBadgeWrapper"
|
||||
>
|
||||
<span
|
||||
class="euiBetaBadge euiBetaBadge--hollow euiCard__betaBadge"
|
||||
id="generated-idBetaBadge"
|
||||
title="Recommended"
|
||||
>
|
||||
Recommended
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`NoDataCard renders 1`] = `
|
||||
<div
|
||||
class="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiCard euiCard--centerAligned"
|
||||
>
|
||||
<div
|
||||
class="euiCard__content"
|
||||
>
|
||||
<span
|
||||
class="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
Card title
|
||||
</span>
|
||||
<div
|
||||
class="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -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(<ElasticAgentCard solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
test('recommended', () => {
|
||||
const component = shallow(<ElasticAgentCard recommended solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('button', () => {
|
||||
const component = shallow(<ElasticAgentCard button="Button" solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('href', () => {
|
||||
const component = shallow(<ElasticAgentCard href="#" button="Button" solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ElasticAgentCardProps> = ({
|
||||
solution,
|
||||
recommended = true,
|
||||
href = 'app/integrations/browse',
|
||||
button,
|
||||
...cardRest
|
||||
}) => {
|
||||
const {
|
||||
services: { http },
|
||||
} = useKibana<CoreStart>();
|
||||
const addBasePath = http.basePath.prepend;
|
||||
const basePathUrl = '/plugins/kibanaReact/assets/';
|
||||
|
||||
const footer =
|
||||
typeof button !== 'string' && typeof button !== 'undefined' ? (
|
||||
button
|
||||
) : (
|
||||
<EuiButton href={href} onClick={cardRest?.onClick} target={cardRest?.target} fill>
|
||||
{button ||
|
||||
i18n.translate('kibana-react.noDataPage.elasticAgentCard.buttonLabel', {
|
||||
defaultMessage: 'Find an integration for {solution}',
|
||||
values: { solution },
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiCard
|
||||
paddingSize="l"
|
||||
href={href}
|
||||
title={i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', {
|
||||
defaultMessage: 'Add a {solution} integration',
|
||||
values: { solution },
|
||||
})}
|
||||
description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', {
|
||||
defaultMessage: `The Elastic Agent provides a simple, unified way to
|
||||
collect data from your machines.`,
|
||||
})}
|
||||
image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)}
|
||||
betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined}
|
||||
footer={footer}
|
||||
{...(cardRest as any)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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(<ElasticBeatsCard solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
test('recommended', () => {
|
||||
const component = shallow(<ElasticBeatsCard recommended solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('button', () => {
|
||||
const component = shallow(<ElasticBeatsCard button="Button" solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('href', () => {
|
||||
const component = shallow(<ElasticBeatsCard href="#" button="Button" solution="Solution" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ElasticBeatsCardProps> = ({
|
||||
recommended,
|
||||
href = 'app/home#/tutorial',
|
||||
button,
|
||||
solution,
|
||||
...cardRest
|
||||
}) => {
|
||||
const {
|
||||
services: { http, uiSettings },
|
||||
} = useKibana<CoreStart>();
|
||||
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
|
||||
) : (
|
||||
<EuiButton href={href} onClick={cardRest?.onClick} target={cardRest?.target} fill>
|
||||
{button ||
|
||||
i18n.translate('kibana-react.noDataPage.elasticBeatsCard.buttonLabel', {
|
||||
defaultMessage: 'Install Beats for {solution}',
|
||||
values: { solution },
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiCard
|
||||
paddingSize="l"
|
||||
href={href}
|
||||
title={i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', {
|
||||
defaultMessage: 'Add data with Beats',
|
||||
})}
|
||||
description={i18n.translate('kibana-react.noDataPage.elasticBeatsCard.description', {
|
||||
defaultMessage: `
|
||||
Beats send data from hundreds or thousands of machines and systems to Logstash or Elasticsearch.`,
|
||||
})}
|
||||
image={addBasePath(
|
||||
`${basePathUrl}elastic_beats_card_${IS_DARK_THEME ? 'dark' : 'light'}.svg`
|
||||
)}
|
||||
betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined}
|
||||
footer={footer}
|
||||
{...(cardRest as any)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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(<NoDataCard title="Card title" description="Description" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
test('recommended', () => {
|
||||
const component = render(
|
||||
<NoDataCard recommended title="Card title" description="Description" />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('button', () => {
|
||||
const component = render(
|
||||
<NoDataCard button="Button" title="Card title" description="Description" />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('href', () => {
|
||||
const component = render(
|
||||
<NoDataCard href="#" button="Button" title="Card title" description="Description" />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<NoDataPageActions> = ({
|
||||
recommended,
|
||||
button,
|
||||
...cardRest
|
||||
}) => {
|
||||
const footer =
|
||||
typeof button !== 'string' ? (
|
||||
button
|
||||
) : (
|
||||
<EuiButton href={cardRest?.href} onClick={cardRest?.onClick} target={cardRest?.target} fill>
|
||||
{button}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiCard
|
||||
paddingSize="l"
|
||||
betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined}
|
||||
footer={footer}
|
||||
{...(cardRest as any)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
.kbnNoDataPageContents__item:only-child {
|
||||
min-width: 400px;
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
<NoDataPage
|
||||
solution="Elastic"
|
||||
actions={{
|
||||
elasticAgent: {},
|
||||
beats: {},
|
||||
custom: {},
|
||||
}}
|
||||
docsLink="test"
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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<EuiCardProps> & {
|
||||
/**
|
||||
* 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<HTMLElement>;
|
||||
};
|
||||
|
||||
export type NoDataPageActionsProps = Record<string, NoDataPageActions>;
|
||||
|
||||
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<NoDataPageProps> = ({
|
||||
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 (
|
||||
<EuiFlexItem key={`empty-page-agent-action`} className="kbnNoDataPageContents__item">
|
||||
<ElasticAgentCard solution={solution} {...action} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
} else if (actionsKeys[i] === 'beats') {
|
||||
return (
|
||||
<EuiFlexItem key={`empty-page-beats-action`} className="kbnNoDataPageContents__item">
|
||||
<ElasticBeatsCard solution={solution} {...action} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`empty-page-${actionsKeys[i]}-action`}
|
||||
className="kbnNoDataPageContents__item"
|
||||
>
|
||||
<NoDataCard {...action} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [actions, sortedData, actionsKeys]);
|
||||
|
||||
return (
|
||||
<div className="kbnNoDataPageContents">
|
||||
<EuiText textAlign="center">
|
||||
<KibanaPageTemplateSolutionNavAvatar
|
||||
name={solution}
|
||||
iconType={logo || `logo${solution}`}
|
||||
size="xxl"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<h1>
|
||||
{pageTitle || (
|
||||
<FormattedMessage
|
||||
id="kibana-react.noDataPage.welcomeTitle"
|
||||
defaultMessage="Welcome to Elastic {solution}!"
|
||||
values={{ solution }}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
<EuiTextColor color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kibana-react.noDataPage.intro"
|
||||
defaultMessage="Add your data to get started, or {link} about {solution}."
|
||||
values={{
|
||||
solution,
|
||||
link: (
|
||||
<EuiLink href={docsLink}>
|
||||
<FormattedMessage
|
||||
id="kibana-react.noDataPage.intro.link"
|
||||
defaultMessage="learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGrid columns={2} style={{ justifyContent: 'space-around' }}>
|
||||
{renderActions}
|
||||
</EuiFlexGrid>
|
||||
{actionsKeys.length > 1 ? (
|
||||
<>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiText textAlign="center" color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="kibana-react.noDataPage.cantDecide"
|
||||
defaultMessage="Confused on which to use? {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink href="https://www.elastic.co/guide/en/fleet/current/beats-agent-comparison.html">
|
||||
<FormattedMessage
|
||||
id="kibana-react.noDataPage.cantDecide.link"
|
||||
defaultMessage="Check our docs for more information."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -10,4 +10,8 @@
|
|||
&.kbnPageTemplate__pageSideBar--shrink {
|
||||
min-width: $euiSizeXXL;
|
||||
}
|
||||
|
||||
.kbnPageTemplate--centeredBody & {
|
||||
border-right: $euiBorderThin;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
<KibanaPageTemplate
|
||||
pageHeader={{
|
||||
iconType: 'test',
|
||||
title: 'test',
|
||||
description: 'test',
|
||||
rightSideItems: ['test'],
|
||||
}}
|
||||
solutionNav={{
|
||||
name: 'Solution',
|
||||
icon: 'solution',
|
||||
items: navItems,
|
||||
}}
|
||||
noDataConfig={noDataConfig}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render sidebar classes', () => {
|
||||
const component = shallow(
|
||||
<KibanaPageTemplate
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
KibanaPageTemplateSolutionNavProps,
|
||||
} from './solution_nav/solution_nav';
|
||||
|
||||
import { NoDataPage, NoDataPageProps, NO_DATA_PAGE_TEMPLATE_PROPS } from './no_data_page';
|
||||
|
||||
/**
|
||||
* A thin wrapper around EuiPageTemplate with a few Kibana specific additions
|
||||
*/
|
||||
|
@ -39,10 +41,16 @@ export type KibanaPageTemplateProps = EuiPageTemplateProps & {
|
|||
* 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;
|
||||
};
|
||||
|
||||
export const KibanaPageTemplate: FunctionComponent<KibanaPageTemplateProps> = ({
|
||||
template,
|
||||
className,
|
||||
pageHeader,
|
||||
children,
|
||||
isEmptyState,
|
||||
|
@ -50,6 +58,7 @@ export const KibanaPageTemplate: FunctionComponent<KibanaPageTemplateProps> = ({
|
|||
pageSideBar,
|
||||
pageSideBarProps,
|
||||
solutionNav,
|
||||
noDataConfig,
|
||||
...rest
|
||||
}) => {
|
||||
/**
|
||||
|
@ -86,11 +95,10 @@ export const KibanaPageTemplate: FunctionComponent<KibanaPageTemplateProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
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<KibanaPageTemplateProps> = ({
|
|||
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 (
|
||||
<EuiPageTemplate
|
||||
template={template}
|
||||
className={classes}
|
||||
pageSideBar={pageSideBar}
|
||||
pageSideBarProps={{
|
||||
paddingSize: solutionNav ? 'none' : 'l',
|
||||
...pageSideBarProps,
|
||||
className: classNames(sideBarClasses, pageSideBarProps?.className),
|
||||
}}
|
||||
{...NO_DATA_PAGE_TEMPLATE_PROPS}
|
||||
>
|
||||
<NoDataPage {...noDataConfig} />
|
||||
</EuiPageTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPageTemplate
|
||||
template={template}
|
||||
className={classes}
|
||||
restrictWidth={restrictWidth}
|
||||
pageHeader={pageHeader}
|
||||
pageSideBar={pageSideBar}
|
||||
|
|
|
@ -14,6 +14,10 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7);
|
|||
width: 248px;
|
||||
padding: $euiSizeL;
|
||||
}
|
||||
|
||||
.kbnPageTemplateSolutionNavAvatar {
|
||||
margin-right: $euiSize;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnPageTemplateSolutionNav--hidden {
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
.kbnPageTemplateSolutionNavAvatar {
|
||||
@include euiBottomShadowSmall;
|
||||
margin-right: $euiSize;
|
||||
|
||||
&--xxl {
|
||||
@include euiBottomShadowMedium;
|
||||
@include size(100px);
|
||||
line-height: 100px;
|
||||
border-radius: 100px;
|
||||
display: inline-block;
|
||||
background: $euiColorEmptyShade url('../../assets/texture.svg') no-repeat;
|
||||
background-size: cover, 125%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,21 +10,36 @@ import './solution_nav_avatar.scss';
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { EuiAvatar, EuiAvatarProps } from '@elastic/eui';
|
||||
import { DistributiveOmit, EuiAvatar, EuiAvatarProps } from '@elastic/eui';
|
||||
|
||||
export type KibanaPageTemplateSolutionNavAvatarProps = EuiAvatarProps;
|
||||
export type KibanaPageTemplateSolutionNavAvatarProps = DistributiveOmit<EuiAvatarProps, 'size'> & {
|
||||
/**
|
||||
* 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<KibanaPageTemplateSolutionNavAvatarProps> = ({
|
||||
className,
|
||||
size,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
// @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine
|
||||
<EuiAvatar
|
||||
className={classNames('kbnPageTemplateSolutionNavAvatar', className)}
|
||||
className={classNames(
|
||||
'kbnPageTemplateSolutionNavAvatar',
|
||||
{
|
||||
[`kbnPageTemplateSolutionNavAvatar--${size}`]: size,
|
||||
},
|
||||
className
|
||||
)}
|
||||
color="plain"
|
||||
size={size === 'xxl' ? 'xl' : size}
|
||||
iconSize={size}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`Storyshots Home Home Page 1`] = `
|
||||
<div
|
||||
className="euiPage euiPage--grow euiPageTemplate"
|
||||
className="euiPage euiPage--grow euiPageTemplate kbnPageTemplate"
|
||||
style={
|
||||
Object {
|
||||
"minHeight": 460,
|
||||
|
|
Loading…
Reference in a new issue