From 1c3aa7ae1e66c91fff6343e28bdbd6c84cbbc21c Mon Sep 17 00:00:00 2001
From: Maja Grubic
Date: Mon, 11 Apr 2022 20:28:07 +0200
Subject: [PATCH 1/6] [SharedUX] Migrate PageTemplate component (#129323)
* [SharedUX] Migrate PageTemplate > SolutionNavAvatar
* [SharedUX] Migrate PageTemplate > NoDataPage > ActionCards
* [SharedUX] Migrate PageTemplate > NoDataPage > SolutionNav
* Updating snapshot
* Fix i18n
* Fix index.tsx
* Fix failing test
* Remove unnecessary export
* Change folder structure of solution_nav_avatar
* [SharedUX] Migrate PageTemplate > NoDataPage > ActionCards
* [SharedUX] Migrate PageTemplate > NoDataPage > SolutionNav
* Updating snapshot
* Fix index.tsx
* Fix failing test
* Remove unnecessary export
* Fix comment
* Renaming component
* Fix folder structure
* Fix storybook
* Style fix
* Fix SolutionAvatar reference
* Fix failing test
* Fix failing snapshot
* Applying PR comments
* Updating failing snapshot
* Extract i18n
* collapsed > isCollapsed
* Applying PR comments
* NoDataConfigPage
* [SharedUX] Migrate PageTemplate
* Export component
* Quick design fixes
* Add pageHeader as a storybook prop
* Remove build additions
* Update .mdx file
* Merge two stories
* Applying Clint's comments
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: cchaos
---
.../kbn-shared-ux-components/src/index.ts | 17 ++
.../__snapshots__/page_template.test.tsx.snap | 197 ++++++++++++++++++
.../page_template_inner.test.tsx.snap | 85 ++++++++
.../with_solution_nav.test.tsx.snap | 125 +++++++++++
.../assets/kibana_template_no_data_config.png | 0
.../src/page_template/index.ts | 12 ++
.../src/page_template/no_data_page/index.ts | 1 +
.../no_data_config_page.test.tsx.snap | 31 +++
.../no_data_config_page}/index.tsx | 3 +-
.../no_data_config_page.test.tsx | 30 +++
.../no_data_config_page.tsx | 38 ++++
.../src/page_template/page_template.mdx | 168 +++++++++++++++
.../src/page_template/page_template.scss | 19 ++
.../page_template/page_template.stories.tsx | 143 +++++++++++++
.../src/page_template/page_template.test.tsx | 113 ++++++++++
.../src/page_template/page_template.tsx | 69 ++++++
.../page_template_inner.test.tsx | 67 ++++++
.../src/page_template/page_template_inner.tsx | 67 ++++++
.../src/page_template/types.ts | 30 +++
.../src/page_template/util/constants.ts | 21 ++
.../src/page_template/util/index.ts | 10 +
.../src/page_template/util/presentation.ts | 13 ++
.../page_template/with_solution_nav.test.tsx | 80 +++++++
.../src/page_template/with_solution_nav.tsx | 76 +++++++
24 files changed, 1413 insertions(+), 2 deletions(-)
create mode 100644 packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template.test.tsx.snap
create mode 100644 packages/kbn-shared-ux-components/src/page_template/__snapshots__/page_template_inner.test.tsx.snap
create mode 100644 packages/kbn-shared-ux-components/src/page_template/__snapshots__/with_solution_nav.test.tsx.snap
create mode 100644 packages/kbn-shared-ux-components/src/page_template/assets/kibana_template_no_data_config.png
create mode 100644 packages/kbn-shared-ux-components/src/page_template/index.ts
create mode 100644 packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/__snapshots__/no_data_config_page.test.tsx.snap
rename packages/kbn-shared-ux-components/src/page_template/{ => no_data_page/no_data_config_page}/index.tsx (76%)
create mode 100644 packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.test.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_config_page/no_data_config_page.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template.mdx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template.scss
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template.stories.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template.test.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template_inner.test.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/page_template_inner.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/types.ts
create mode 100644 packages/kbn-shared-ux-components/src/page_template/util/constants.ts
create mode 100644 packages/kbn-shared-ux-components/src/page_template/util/index.ts
create mode 100644 packages/kbn-shared-ux-components/src/page_template/util/presentation.ts
create mode 100644 packages/kbn-shared-ux-components/src/page_template/with_solution_nav.test.tsx
create mode 100644 packages/kbn-shared-ux-components/src/page_template/with_solution_nav.tsx
diff --git a/packages/kbn-shared-ux-components/src/index.ts b/packages/kbn-shared-ux-components/src/index.ts
index a022d2d0c755d..e6e72b23c3d17 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
+ ,
+ ],
+ }}
+/>
+```
+
+![Screenshot of demo empty state code. Shows the Kibana navigation bars and a centered empty state with the dashboard app icon, a level 1 heading "Dashboards", body text "You don't have any dashboards yet.", and a button that says "Create new dashboard".](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/assets/kibana_default_empty_state.png)
+
+
+ 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
+ ,
+ ]}
+ />
+
+```
+
+![Screenshot of demo custom empty state code. Shows the Kibana navigation bars and a centered empty state with the a level 1 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/assets/kibana_custom_empty_state.png)
+
+### 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
+ ,
+ ]}
+ />
+
+```
+
+![Screenshot of demo custom empty state code with a page header. Shows the Kibana navigation bars, a level 1 heading "Dashboards", and a centered empty state with the a level 2 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/assets/kibana_header_and_empty_state.png)
+
+## `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
+
+ {...}
+
+```
+
+![Screenshot of Stack Management empty state with a provided solution navigation shown on the left, outlined in pink.](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/assets/kibana_template_solution_nav.png)
+
+![Screenshots of Stack Management page in mobile view. Menu closed on the left, menu open on the right.](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/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 [`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 */}
+
+```
+
+![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.](https://raw.githubusercontent.com/elastic/kibana/main/dev_docs/assets/kibana_template_no_data_config.png)
\ 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;
+};
From f20218f57078c38e7245a64b9edc5d5dea69ab7f Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Mon, 11 Apr 2022 11:59:38 -0700
Subject: [PATCH 2/6] [Security Solution] Fix Endpoint count in policy list,
text updates (#129907)
---
.../management/pages/policy/view/policy_list.tsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
index 44649936c490a..bdc0c7d779b15 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -89,7 +89,11 @@ export const PolicyList = memo(() => {
const policyIdToEndpointCount = useMemo(() => {
const map = new Map();
for (const policy of endpointCount?.items) {
- map.set(policy.package_policies[0], policy.agents ?? 0);
+ for (const packagePolicyId of policy.package_policies) {
+ if (policyIds.includes(packagePolicyId as string)) {
+ map.set(packagePolicyId, policy.agents ?? 0);
+ }
+ }
}
// error with the endpointCount api call, set default count to 0
@@ -289,11 +293,11 @@ export const PolicyList = memo(() => {
data-test-subj="policyListPage"
hideHeader={totalItemCount === 0}
title={i18n.translate('xpack.securitySolution.policy.list.title', {
- defaultMessage: 'Policy List',
+ defaultMessage: 'Policies',
})}
subtitle={i18n.translate('xpack.securitySolution.policy.list.subtitle', {
defaultMessage:
- 'Use endpoint policies to customize endpoint security protections and other configurations',
+ 'Use policies to customize endpoint and cloud workload protections and other configurations',
})}
>
{totalItemCount > 0 ? (
From 6954b0ff6b51a220498ac7926fad9751731cdea7 Mon Sep 17 00:00:00 2001
From: Andrew Tate
Date: Mon, 11 Apr 2022 14:09:02 -0500
Subject: [PATCH 3/6] [Discover] make field icons consistent across field list
and doc tables (#129621)
---
.../components/sidebar/discover_field.tsx | 8 ++--
.../doc_viewer_table/legacy/table.tsx | 7 +++-
.../components/doc_viewer_table/table.tsx | 7 +++-
.../utils/get_type_for_field_icon.test.ts | 39 +++++++++++++++++++
.../public/utils/get_type_for_field_icon.ts | 17 ++++++++
5 files changed, 73 insertions(+), 5 deletions(-)
create mode 100644 src/plugins/discover/public/utils/get_type_for_field_icon.test.ts
create mode 100644 src/plugins/discover/public/utils/get_type_for_field_icon.ts
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
index e629c85c6d242..e14d9f7a0e5a7 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
@@ -25,6 +25,7 @@ import { i18n } from '@kbn/i18n';
import { UiCounterMetricType } from '@kbn/analytics';
import classNames from 'classnames';
import { FieldButton, FieldIcon } from '@kbn/react-field';
+import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon';
import { DiscoverFieldDetails } from './discover_field_details';
import { FieldDetails } from './types';
import type { DataViewField, DataView } from '../../../../../../data_views/public';
@@ -59,9 +60,10 @@ const FieldInfoIcon: React.FC = memo(() => (
));
const DiscoverFieldTypeIcon: React.FC<{ field: DataViewField }> = memo(({ field }) => {
- // If it's a string type, we want to distinguish between keyword and text
- const tempType = field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type;
- return ;
+ const typeForIcon = getTypeForFieldIcon(field);
+ return (
+
+ );
});
const FieldName: React.FC<{ field: DataViewField }> = memo(({ field }) => {
diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx
index aab4856d6698c..aa44f0d56889c 100644
--- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx
+++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx
@@ -9,6 +9,7 @@
import '../table.scss';
import React, { useCallback, useMemo } from 'react';
import { EuiInMemoryTable } from '@elastic/eui';
+import { getTypeForFieldIcon } from '../../../../../utils/get_type_for_field_icon';
import { useDiscoverServices } from '../../../../../utils/use_discover_services';
import { flattenHit } from '../../../../../../../data/public';
import { SHOW_MULTIFIELDS } from '../../../../../../common';
@@ -73,7 +74,11 @@ export const DocViewerLegacyTable = ({
.map((field) => {
const fieldMapping = mapping(field);
const displayName = fieldMapping?.displayName ?? field;
- const fieldType = isNestedFieldParent(field, dataView) ? 'nested' : fieldMapping?.type;
+ const fieldType = isNestedFieldParent(field, dataView)
+ ? 'nested'
+ : fieldMapping
+ ? getTypeForFieldIcon(fieldMapping)
+ : undefined;
const ignored = getIgnoredReason(fieldMapping ?? field, hit._ignored);
return {
action: {
diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx
index 7aa372e36adff..4742dd3d8f585 100644
--- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx
+++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx
@@ -27,6 +27,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { debounce } from 'lodash';
+import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon';
import { useDiscoverServices } from '../../../../utils/use_discover_services';
import { Storage } from '../../../../../../kibana_utils/public';
import { usePager } from '../../../../utils/use_pager';
@@ -157,7 +158,11 @@ export const DocViewerTable = ({
(field: string) => {
const fieldMapping = mapping(field);
const displayName = fieldMapping?.displayName ?? field;
- const fieldType = isNestedFieldParent(field, dataView) ? 'nested' : fieldMapping?.type;
+ const fieldType = isNestedFieldParent(field, dataView)
+ ? 'nested'
+ : fieldMapping
+ ? getTypeForFieldIcon(fieldMapping)
+ : undefined;
const ignored = getIgnoredReason(fieldMapping ?? field, hit._ignored);
diff --git a/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts b/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts
new file mode 100644
index 0000000000000..bffa0fecaed74
--- /dev/null
+++ b/src/plugins/discover/public/utils/get_type_for_field_icon.test.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { DataViewField } from 'src/plugins/data_views/common';
+import { getTypeForFieldIcon } from './get_type_for_field_icon';
+
+describe('getTypeForFieldIcon', () => {
+ it('extracts type for non-string types', () => {
+ expect(
+ getTypeForFieldIcon({
+ type: 'not-string',
+ esTypes: ['bar'],
+ } as DataViewField)
+ ).toBe('not-string');
+ });
+
+ it('extracts type when type is string but esTypes is unavailable', () => {
+ expect(
+ getTypeForFieldIcon({
+ type: 'string',
+ esTypes: undefined,
+ } as DataViewField)
+ ).toBe('string');
+ });
+
+ it('extracts esType when type is string and esTypes is available', () => {
+ expect(
+ getTypeForFieldIcon({
+ type: 'string',
+ esTypes: ['version'],
+ } as DataViewField)
+ ).toBe('version');
+ });
+});
diff --git a/src/plugins/discover/public/utils/get_type_for_field_icon.ts b/src/plugins/discover/public/utils/get_type_for_field_icon.ts
new file mode 100644
index 0000000000000..bb3f65ed3e01c
--- /dev/null
+++ b/src/plugins/discover/public/utils/get_type_for_field_icon.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { DataViewField } from 'src/plugins/data_views/common';
+
+/**
+ * Extracts the type from a data view field that will match the right icon.
+ *
+ * We define custom logic for Discover in order to distinguish between various "string" types.
+ */
+export const getTypeForFieldIcon = (field: DataViewField) =>
+ field.type === 'string' && field.esTypes ? field.esTypes[0] : field.type;
From 837813eef89913e422f9c2b0a7b3859fa2ff5c51 Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Mon, 11 Apr 2022 13:09:36 -0600
Subject: [PATCH 4/6] [Logs UI][Rules] Add integration tests for Log Threshold
Rule (#128019)
* [Logs UI][Rules] Add integration tests for Log Threshold Rule
* Fixing tests
* Add timestamp to preview calls
* Update test to reflect new argument count
---
.../log_threshold_chart_preview.ts | 23 +-
.../log_threshold_executor.test.ts | 16 +-
.../log_threshold/log_threshold_executor.ts | 73 ++++--
.../api_integration/apis/logs_ui/index.ts | 1 +
.../apis/logs_ui/log_threshold_alert.ts | 236 ++++++++++++++++++
5 files changed, 321 insertions(+), 28 deletions(-)
create mode 100644 x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts
index 56bbd69240dc2..4483ad30c0246 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts
@@ -47,11 +47,28 @@ export async function getChartPreviewData(
timeSize: timeSize * buckets,
};
- const { rangeFilter } = buildFiltersFromCriteria(expandedAlertParams, timestampField);
+ const executionTimestamp = Date.now();
+ const { rangeFilter } = buildFiltersFromCriteria(
+ expandedAlertParams,
+ timestampField,
+ executionTimestamp
+ );
const query = isGrouped
- ? getGroupedESQuery(expandedAlertParams, timestampField, indices, runtimeMappings)
- : getUngroupedESQuery(expandedAlertParams, timestampField, indices, runtimeMappings);
+ ? getGroupedESQuery(
+ expandedAlertParams,
+ timestampField,
+ indices,
+ runtimeMappings,
+ executionTimestamp
+ )
+ : getUngroupedESQuery(
+ expandedAlertParams,
+ timestampField,
+ indices,
+ runtimeMappings,
+ executionTimestamp
+ );
if (!query) {
throw new Error('ES query could not be built from the provided alert params');
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts
index d6b599336fac8..78934d09755be 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts
@@ -134,6 +134,7 @@ const baseRuleParams: Pick = {
const TIMESTAMP_FIELD = '@timestamp';
const FILEBEAT_INDEX = 'filebeat-*';
+const EXECUTION_TIMESTAMP = new Date('2022-01-01T00:00:00.000Z').valueOf();
const runtimeMappings: estypes.MappingRuntimeFields = {
runtime_field: {
@@ -166,7 +167,7 @@ describe('Log threshold executor', () => {
...baseRuleParams,
criteria: positiveCriteria,
};
- const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD);
+ const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP);
expect(filters.mustFilters).toEqual(expectedPositiveFilterClauses);
});
@@ -175,14 +176,14 @@ describe('Log threshold executor', () => {
...baseRuleParams,
criteria: negativeCriteria,
};
- const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD);
+ const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP);
expect(filters.mustNotFilters).toEqual(expectedNegativeFilterClauses);
});
test('Handles time range', () => {
const ruleParams: RuleParams = { ...baseRuleParams, criteria: [] };
- const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD);
+ const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP);
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number');
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number');
expect(filters.rangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis');
@@ -204,7 +205,8 @@ describe('Log threshold executor', () => {
ruleParams,
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
- runtimeMappings
+ runtimeMappings,
+ EXECUTION_TIMESTAMP
);
expect(query).toEqual({
index: 'filebeat-*',
@@ -254,7 +256,8 @@ describe('Log threshold executor', () => {
ruleParams,
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
- runtimeMappings
+ runtimeMappings,
+ EXECUTION_TIMESTAMP
);
expect(query).toEqual({
@@ -324,7 +327,8 @@ describe('Log threshold executor', () => {
ruleParams,
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
- runtimeMappings
+ runtimeMappings,
+ EXECUTION_TIMESTAMP
);
expect(query).toEqual({
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
index be106226d0ee4..12b523d126f80 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
@@ -118,7 +118,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
: relativeViewInAppUrl;
const sharedContext = {
- timestamp: new Date().toISOString(),
+ timestamp: startedAt.toISOString(),
viewInAppUrl,
};
actions.forEach((actionSet) => {
@@ -149,7 +149,8 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
indices,
runtimeMappings,
scopedClusterClient.asCurrentUser,
- alertFactory
+ alertFactory,
+ startedAt.valueOf()
);
} else {
await executeRatioAlert(
@@ -158,7 +159,8 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
indices,
runtimeMappings,
scopedClusterClient.asCurrentUser,
- alertFactory
+ alertFactory,
+ startedAt.valueOf()
);
}
} catch (e) {
@@ -166,15 +168,22 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
}
});
-async function executeAlert(
+export async function executeAlert(
ruleParams: CountRuleParams,
timestampField: string,
indexPattern: string,
runtimeMappings: estypes.MappingRuntimeFields,
esClient: ElasticsearchClient,
- alertFactory: LogThresholdAlertFactory
+ alertFactory: LogThresholdAlertFactory,
+ executionTimestamp: number
) {
- const query = getESQuery(ruleParams, timestampField, indexPattern, runtimeMappings);
+ const query = getESQuery(
+ ruleParams,
+ timestampField,
+ indexPattern,
+ runtimeMappings,
+ executionTimestamp
+ );
if (!query) {
throw new Error('ES query could not be built from the provided alert params');
@@ -187,13 +196,14 @@ async function executeAlert(
}
}
-async function executeRatioAlert(
+export async function executeRatioAlert(
ruleParams: RatioRuleParams,
timestampField: string,
indexPattern: string,
runtimeMappings: estypes.MappingRuntimeFields,
esClient: ElasticsearchClient,
- alertFactory: LogThresholdAlertFactory
+ alertFactory: LogThresholdAlertFactory,
+ executionTimestamp: number
) {
// Ratio alert params are separated out into two standard sets of alert params
const numeratorParams: RuleParams = {
@@ -206,12 +216,19 @@ async function executeRatioAlert(
criteria: getDenominator(ruleParams.criteria),
};
- const numeratorQuery = getESQuery(numeratorParams, timestampField, indexPattern, runtimeMappings);
+ const numeratorQuery = getESQuery(
+ numeratorParams,
+ timestampField,
+ indexPattern,
+ runtimeMappings,
+ executionTimestamp
+ );
const denominatorQuery = getESQuery(
denominatorParams,
timestampField,
indexPattern,
- runtimeMappings
+ runtimeMappings,
+ executionTimestamp
);
if (!numeratorQuery || !denominatorQuery) {
@@ -247,11 +264,24 @@ const getESQuery = (
alertParams: Omit & { criteria: CountCriteria },
timestampField: string,
indexPattern: string,
- runtimeMappings: estypes.MappingRuntimeFields
+ runtimeMappings: estypes.MappingRuntimeFields,
+ executionTimestamp: number
) => {
return hasGroupBy(alertParams)
- ? getGroupedESQuery(alertParams, timestampField, indexPattern, runtimeMappings)
- : getUngroupedESQuery(alertParams, timestampField, indexPattern, runtimeMappings);
+ ? getGroupedESQuery(
+ alertParams,
+ timestampField,
+ indexPattern,
+ runtimeMappings,
+ executionTimestamp
+ )
+ : getUngroupedESQuery(
+ alertParams,
+ timestampField,
+ indexPattern,
+ runtimeMappings,
+ executionTimestamp
+ );
};
export const processUngroupedResults = (
@@ -452,13 +482,14 @@ export const processGroupByRatioResults = (
export const buildFiltersFromCriteria = (
params: Pick & { criteria: CountCriteria },
- timestampField: string
+ timestampField: string,
+ executionTimestamp: number
) => {
const { timeSize, timeUnit, criteria } = params;
const interval = `${timeSize}${timeUnit}`;
const intervalAsSeconds = getIntervalInSeconds(interval);
const intervalAsMs = intervalAsSeconds * 1000;
- const to = Date.now();
+ const to = executionTimestamp;
const from = to - intervalAsMs;
const positiveComparators = getPositiveComparators();
@@ -511,7 +542,8 @@ export const getGroupedESQuery = (
},
timestampField: string,
index: string,
- runtimeMappings: estypes.MappingRuntimeFields
+ runtimeMappings: estypes.MappingRuntimeFields,
+ executionTimestamp: number
): estypes.SearchRequest | undefined => {
// IMPORTANT:
// For the group by scenario we need to account for users utilizing "less than" configurations
@@ -532,7 +564,8 @@ export const getGroupedESQuery = (
const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria(
params,
- timestampField
+ timestampField,
+ executionTimestamp
);
if (isOptimizableGroupedThreshold(comparator, value)) {
@@ -616,11 +649,13 @@ export const getUngroupedESQuery = (
params: Pick & { criteria: CountCriteria },
timestampField: string,
index: string,
- runtimeMappings: estypes.MappingRuntimeFields
+ runtimeMappings: estypes.MappingRuntimeFields,
+ executionTimestamp: number
): object => {
const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria(
params,
- timestampField
+ timestampField,
+ executionTimestamp
);
const body: estypes.SearchRequest['body'] = {
diff --git a/x-pack/test/api_integration/apis/logs_ui/index.ts b/x-pack/test/api_integration/apis/logs_ui/index.ts
index 125ca65f52734..625ff24ce25cd 100644
--- a/x-pack/test/api_integration/apis/logs_ui/index.ts
+++ b/x-pack/test/api_integration/apis/logs_ui/index.ts
@@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Logs UI routes', () => {
loadTestFile(require.resolve('./log_views'));
+ loadTestFile(require.resolve('./log_threshold_alert'));
});
}
diff --git a/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts b/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts
new file mode 100644
index 0000000000000..18f3a181dde53
--- /dev/null
+++ b/x-pack/test/api_integration/apis/logs_ui/log_threshold_alert.ts
@@ -0,0 +1,236 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import sinon from 'sinon';
+import {
+ executeAlert,
+ executeRatioAlert,
+} from '../../../../plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor';
+import { DATES } from '../metrics_ui/constants';
+import { FtrProviderContext } from '../../ftr_provider_context';
+import {
+ Comparator,
+ TimeUnit,
+ RatioCriteria,
+} from '../../../../plugins/infra/common/alerting/logs/log_threshold/types';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const esClient = getService('es');
+ describe('Log Threshold Rule', () => {
+ describe('executeAlert', () => {
+ before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/alerts_test_data'));
+ after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/alerts_test_data'));
+
+ describe('without group by', () => {
+ it('should work', async () => {
+ const timestamp = new Date(DATES['alert-test-data'].gauge.max);
+ const alertFactory = sinon.fake();
+ const ruleParams = {
+ count: {
+ comparator: Comparator.GT_OR_EQ,
+ value: 1,
+ },
+ timeUnit: 'm' as TimeUnit,
+ timeSize: 5,
+ criteria: [
+ {
+ field: 'env',
+ comparator: Comparator.NOT_EQ,
+ value: 'dev',
+ },
+ ],
+ };
+ await executeAlert(
+ ruleParams,
+ '@timestamp',
+ 'alerts-test-data',
+ {},
+ esClient,
+ alertFactory,
+ timestamp.valueOf()
+ );
+ expect(alertFactory.callCount).to.equal(1);
+ expect(alertFactory.getCall(0).args).to.eql([
+ '*',
+ '2 log entries in the last 5 mins. Alert when ≥ 1.',
+ 2,
+ 1,
+ [
+ {
+ actionGroup: 'logs.threshold.fired',
+ context: {
+ conditions: 'env does not equal dev',
+ group: null,
+ isRatio: false,
+ matchingDocuments: 2,
+ reason: '2 log entries in the last 5 mins. Alert when ≥ 1.',
+ },
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('with group by', () => {
+ it('should work', async () => {
+ const timestamp = new Date(DATES['alert-test-data'].gauge.max);
+ const alertFactory = sinon.fake();
+ const ruleParams = {
+ count: {
+ comparator: Comparator.GT_OR_EQ,
+ value: 1,
+ },
+ timeUnit: 'm' as TimeUnit,
+ timeSize: 5,
+ groupBy: ['env'],
+ criteria: [
+ {
+ field: 'env',
+ comparator: Comparator.NOT_EQ,
+ value: 'test',
+ },
+ ],
+ };
+ await executeAlert(
+ ruleParams,
+ '@timestamp',
+ 'alerts-test-data',
+ {},
+ esClient,
+ alertFactory,
+ timestamp.valueOf()
+ );
+ expect(alertFactory.callCount).to.equal(2);
+ expect(alertFactory.getCall(0).args).to.eql([
+ 'dev',
+ '2 log entries in the last 5 mins for dev. Alert when ≥ 1.',
+ 2,
+ 1,
+ [
+ {
+ actionGroup: 'logs.threshold.fired',
+ context: {
+ conditions: 'env does not equal test',
+ group: 'dev',
+ isRatio: false,
+ matchingDocuments: 2,
+ reason: '2 log entries in the last 5 mins for dev. Alert when ≥ 1.',
+ },
+ },
+ ],
+ ]);
+ });
+ });
+ });
+
+ describe('executeRatioAlert', () => {
+ before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/ten_thousand_plus'));
+ after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/ten_thousand_plus'));
+
+ describe('without group by', () => {
+ it('should work', async () => {
+ const timestamp = new Date(DATES.ten_thousand_plus.max);
+ const alertFactory = sinon.fake();
+ const ruleParams = {
+ count: {
+ comparator: Comparator.GT_OR_EQ,
+ value: 0.5,
+ },
+ timeUnit: 'm' as TimeUnit,
+ timeSize: 5,
+ criteria: [
+ [{ field: 'event.dataset', comparator: Comparator.EQ, value: 'nginx.error' }],
+ [{ field: 'event.dataset', comparator: Comparator.NOT_EQ, value: 'nginx.error' }],
+ ] as RatioCriteria,
+ };
+ await executeRatioAlert(
+ ruleParams,
+ '@timestamp',
+ 'filebeat-*',
+ {},
+ esClient,
+ alertFactory,
+ timestamp.valueOf()
+ );
+ expect(alertFactory.callCount).to.equal(1);
+ expect(alertFactory.getCall(0).args).to.eql([
+ '*',
+ 'The ratio of selected logs is 0.5526081141328578 in the last 5 mins. Alert when ≥ 0.5.',
+ 0.5526081141328578,
+ 0.5,
+ [
+ {
+ actionGroup: 'logs.threshold.fired',
+ context: {
+ denominatorConditions: 'event.dataset does not equal nginx.error',
+ group: null,
+ isRatio: true,
+ numeratorConditions: 'event.dataset equals nginx.error',
+ ratio: 0.5526081141328578,
+ reason:
+ 'The ratio of selected logs is 0.5526081141328578 in the last 5 mins. Alert when ≥ 0.5.',
+ },
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('with group by', () => {
+ it('should work', async () => {
+ const timestamp = new Date(DATES.ten_thousand_plus.max);
+ const alertFactory = sinon.fake();
+ const ruleParams = {
+ count: {
+ comparator: Comparator.GT_OR_EQ,
+ value: 0.5,
+ },
+ timeUnit: 'm' as TimeUnit,
+ timeSize: 5,
+ groupBy: ['event.category'],
+ criteria: [
+ [{ field: 'event.dataset', comparator: Comparator.EQ, value: 'nginx.error' }],
+ [{ field: 'event.dataset', comparator: Comparator.NOT_EQ, value: 'nginx.error' }],
+ ] as RatioCriteria,
+ };
+ await executeRatioAlert(
+ ruleParams,
+ '@timestamp',
+ 'filebeat-*',
+ {},
+ esClient,
+ alertFactory,
+ timestamp.valueOf()
+ );
+ expect(alertFactory.callCount).to.equal(1);
+ expect(alertFactory.getCall(0).args).to.eql([
+ 'web',
+ 'The ratio of selected logs is 0.5526081141328578 in the last 5 mins for web. Alert when ≥ 0.5.',
+ 0.5526081141328578,
+ 0.5,
+ [
+ {
+ actionGroup: 'logs.threshold.fired',
+ context: {
+ denominatorConditions: 'event.dataset does not equal nginx.error',
+ group: 'web',
+ isRatio: true,
+ numeratorConditions: 'event.dataset equals nginx.error',
+ ratio: 0.5526081141328578,
+ reason:
+ 'The ratio of selected logs is 0.5526081141328578 in the last 5 mins for web. Alert when ≥ 0.5.',
+ },
+ },
+ ],
+ ]);
+ });
+ });
+ });
+ });
+}
From 82e7356f027bf9189e6aa172decca559d72622f2 Mon Sep 17 00:00:00 2001
From: Tim Sullivan
Date: Mon, 11 Apr 2022 12:15:01 -0700
Subject: [PATCH 5/6] [Screenshotting] Ensure worker.js content contents are
compiled in build (#129796)
* [Screenshotting] Expose packageInfo to pdf maker for dist data
* somehow use `dist` in the pdfmaker?
* use worker_src_harness exclusively when running from source
* Update pdfmaker.ts
* Update x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts
Co-authored-by: Michael Dokolin
* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'
Co-authored-by: Michael Dokolin
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../screenshotting/server/formats/pdf/index.ts | 4 +++-
.../server/formats/pdf/pdf_maker/index.ts | 6 ++++--
.../integration_tests/pdfmaker.test.ts | 17 +++++++++++++----
.../server/formats/pdf/pdf_maker/pdfmaker.ts | 12 ++++++++++--
.../{worker.js => worker_src_harness.js} | 5 +++++
x-pack/plugins/screenshotting/server/plugin.ts | 5 ++++-
.../server/screenshots/index.test.ts | 14 ++++++++++++--
.../screenshotting/server/screenshots/index.ts | 5 +++--
8 files changed, 54 insertions(+), 14 deletions(-)
rename x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/{worker.js => worker_src_harness.js} (64%)
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
index 7364ec210313a..b7718155c5424 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
@@ -7,7 +7,7 @@
import { groupBy } from 'lodash';
import type { Values } from '@kbn/utility-types';
-import type { Logger } from 'src/core/server';
+import type { Logger, PackageInfo } from 'src/core/server';
import type { LayoutParams } from '../../../common';
import { LayoutTypes } from '../../../common';
import type { Layout } from '../../layouts';
@@ -93,6 +93,7 @@ function getTimeRange(results: CaptureResult['results']) {
export async function toPdf(
logger: Logger,
+ packageInfo: PackageInfo,
layout: Layout,
{ logo, title }: PdfScreenshotOptions,
{ metrics, results }: CaptureResult
@@ -104,6 +105,7 @@ export async function toPdf(
results,
layout,
logo,
+ packageInfo,
logger,
});
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
index ce5ea3cab813c..2c5439267ffc4 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { Logger } from 'src/core/server';
+import type { Logger, PackageInfo } from 'src/core/server';
import { PdfMaker } from './pdfmaker';
import type { Layout } from '../../../layouts';
import { getTracker } from './tracker';
@@ -14,6 +14,7 @@ import type { CaptureResult } from '../../../screenshots';
interface PngsToPdfArgs {
results: CaptureResult['results'];
layout: Layout;
+ packageInfo: PackageInfo;
logger: Logger;
logo?: string;
title?: string;
@@ -24,9 +25,10 @@ export async function pngsToPdf({
layout,
logo,
title,
+ packageInfo,
logger,
}: PngsToPdfArgs): Promise<{ buffer: Buffer; pages: number }> {
- const pdfMaker = new PdfMaker(layout, logo, logger);
+ const pdfMaker = new PdfMaker(layout, logo, packageInfo, logger);
const tracker = getTracker();
if (title) {
pdfMaker.setTitle(title);
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts
index d3c9f2003dd4e..e08cf808abbc5 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts
@@ -7,11 +7,12 @@
/* eslint-disable max-classes-per-file */
+import { PackageInfo } from 'kibana/server';
import path from 'path';
import { loggingSystemMock } from 'src/core/server/mocks';
import { isUint8Array } from 'util/types';
-import { createMockLayout } from '../../../../layouts/mock';
import { errors } from '../../../../../common';
+import { createMockLayout } from '../../../../layouts/mock';
import { PdfMaker } from '../pdfmaker';
const imageBase64 = Buffer.from(
@@ -23,11 +24,19 @@ describe('PdfMaker', () => {
let layout: ReturnType;
let pdf: PdfMaker;
let logger: ReturnType;
+ let packageInfo: Readonly;
beforeEach(() => {
layout = createMockLayout();
logger = loggingSystemMock.createLogger();
- pdf = new PdfMaker(layout, undefined, logger);
+ packageInfo = {
+ branch: 'screenshot-test',
+ buildNum: 567891011,
+ buildSha: 'screenshot-dfdfed0a',
+ dist: false,
+ version: '1000.0.0',
+ };
+ pdf = new PdfMaker(layout, undefined, packageInfo, logger);
});
describe('generate', () => {
@@ -56,14 +65,14 @@ describe('PdfMaker', () => {
protected workerMaxOldHeapSizeMb = 2;
protected workerMaxYoungHeapSizeMb = 2;
protected workerModulePath = path.resolve(__dirname, './memory_leak_worker.js');
- })(layout, undefined, logger);
+ })(layout, undefined, packageInfo, logger);
await expect(leakyMaker.generate()).rejects.toBeInstanceOf(errors.PdfWorkerOutOfMemoryError);
});
it('restarts the PDF worker if it crashes', async () => {
const buggyMaker = new (class BuggyPdfMaker extends PdfMaker {
protected workerModulePath = path.resolve(__dirname, './buggy_worker.js');
- })(layout, undefined, logger);
+ })(layout, undefined, packageInfo, logger);
await expect(buggyMaker.generate()).rejects.toThrowError(new Error('This is a bug'));
await expect(buggyMaker.generate()).rejects.toThrowError(new Error('This is a bug'));
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts
index f32bec1e3ed38..a10f259e5c9e5 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { Logger } from 'src/core/server';
+import type { Logger, PackageInfo } from 'src/core/server';
import { SerializableRecord } from '@kbn/utility-types';
import path from 'path';
import { Content, ContentImage, ContentText } from 'pdfmake/interfaces';
@@ -34,7 +34,7 @@ export class PdfMaker {
private worker?: Worker;
private pageCount: number = 0;
- protected workerModulePath = path.resolve(__dirname, './worker.js');
+ protected workerModulePath: string;
/**
* The maximum heap size for old memory region of the worker thread.
@@ -65,10 +65,18 @@ export class PdfMaker {
constructor(
private readonly layout: Layout,
private readonly logo: string | undefined,
+ { dist }: PackageInfo,
private readonly logger: Logger
) {
this.title = '';
this.content = [];
+
+ // running in dist: `worker.ts` becomes `worker.js`
+ // running in source: `worker_src_harness.ts` needs to be wrapped in JS and have a ts-node environment initialized.
+ this.workerModulePath = path.resolve(
+ __dirname,
+ dist ? './worker.js' : './worker_src_harness.js'
+ );
}
_addContents(contents: Content[]) {
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker.js b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker_src_harness.js
similarity index 64%
rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker.js
rename to x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker_src_harness.js
index d3dfa3e9accf8..e180c130bc4af 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker.js
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/worker_src_harness.js
@@ -5,5 +5,10 @@
* 2.0.
*/
+/**
+ * This file is the harness for importing worker.ts with Kibana running in dev mode.
+ * The TS file needs to be compiled on the fly, unlike when Kibana is running as a dist.
+ */
+
require('../../../../../../../src/setup_node_env');
require('./worker.ts');
diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts
index 3f97de22d5e4b..2f675335d4393 100755
--- a/x-pack/plugins/screenshotting/server/plugin.ts
+++ b/x-pack/plugins/screenshotting/server/plugin.ts
@@ -11,6 +11,7 @@ import type {
CoreSetup,
CoreStart,
Logger,
+ PackageInfo,
Plugin,
PluginInitializerContext,
} from 'src/core/server';
@@ -45,6 +46,7 @@ export interface ScreenshottingStart {
export class ScreenshottingPlugin implements Plugin {
private config: ConfigType;
private logger: Logger;
+ private packageInfo: PackageInfo;
private screenshotMode!: ScreenshotModePluginSetup;
private browserDriverFactory!: Promise;
private screenshots!: Promise;
@@ -52,6 +54,7 @@ export class ScreenshottingPlugin implements Plugin) {
this.logger = context.logger.get();
this.config = context.config.get();
+ this.packageInfo = context.env.packageInfo;
}
setup({ http }: CoreSetup, { screenshotMode }: SetupDeps) {
@@ -82,7 +85,7 @@ export class ScreenshottingPlugin implements Plugin {});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts
index 84a0974b0f8bc..eef98ceea34ba 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts
@@ -6,7 +6,7 @@
*/
import { of, throwError } from 'rxjs';
-import type { Logger } from 'src/core/server';
+import type { Logger, PackageInfo } from 'src/core/server';
import { httpServiceMock } from 'src/core/server/mocks';
import {
SCREENSHOTTING_APP_ID,
@@ -31,6 +31,7 @@ describe('Screenshot Observable Pipeline', () => {
let http: ReturnType;
let layout: ReturnType;
let logger: jest.Mocked;
+ let packageInfo: Readonly;
let options: ScreenshotOptions;
let screenshots: Screenshots;
@@ -44,6 +45,13 @@ describe('Screenshot Observable Pipeline', () => {
error: jest.fn(),
info: jest.fn(),
} as unknown as jest.Mocked;
+ packageInfo = {
+ branch: 'screenshot-test',
+ buildNum: 567891011,
+ buildSha: 'screenshot-dfdfed0a',
+ dist: false,
+ version: '5000.0.0',
+ };
options = {
browserTimezone: 'UTC',
headers: {},
@@ -56,7 +64,9 @@ describe('Screenshot Observable Pipeline', () => {
},
urls: ['/welcome/home/start/index.htm'],
} as unknown as typeof options;
- screenshots = new Screenshots(driverFactory, logger, http, { poolSize: 1 } as ConfigType);
+ screenshots = new Screenshots(driverFactory, logger, packageInfo, http, {
+ poolSize: 1,
+ } as ConfigType);
jest.spyOn(Layouts, 'createLayout').mockReturnValue(layout);
diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts
index 8672babe8b514..cfd5c76093d3b 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/index.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts
@@ -22,7 +22,7 @@ import {
tap,
toArray,
} from 'rxjs/operators';
-import type { HttpServiceSetup, KibanaRequest, Logger } from 'src/core/server';
+import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from 'src/core/server';
import type { ExpressionAstExpression } from 'src/plugins/expressions/common';
import {
LayoutParams,
@@ -99,6 +99,7 @@ export class Screenshots {
constructor(
private readonly browserDriverFactory: HeadlessChromiumDriverFactory,
private readonly logger: Logger,
+ private readonly packageInfo: PackageInfo,
private readonly http: HttpServiceSetup,
{ poolSize }: ConfigType
) {
@@ -214,7 +215,7 @@ export class Screenshots {
mergeMap((result) => {
switch (options.format) {
case 'pdf':
- return toPdf(this.logger, layout, options, result);
+ return toPdf(this.logger, this.packageInfo, layout, options, result);
default:
return toPng(result);
}
From 0f7179a1438563c76a6bb5617d0af06be42c71d0 Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Mon, 11 Apr 2022 12:24:53 -0700
Subject: [PATCH 6/6] [Security Solution] Update advanced Policy check for
capture_mode (#129926)
---
.../management/pages/policy/models/advanced_policy_schema.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
index d5147f58d4f0b..7f86540e36426 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts
@@ -880,7 +880,7 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [
'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.kernel.capture_mode',
{
defaultMessage:
- 'Allows users to control whether kprobes or ebpf are used to gather data. Possible options are kprobes, ebpf, or auto. Default: auto',
+ 'Allows users to control whether kprobes or ebpf are used to gather data. Possible options are kprobes, ebpf, or auto. Default: kprobes',
}
),
},