From fe75fbd55d2dc6e77435c66561ba381238dcb9e0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 24 Apr 2023 08:14:48 -0400 Subject: [PATCH 01/36] [Cases] Rounding the file size to avoid decimals (#155542) This PR fixes a bug in the telemetry code around the average file size. A float can be returned by the average aggregation. To avoid the float we'll round it before saving the value in the document. --- .../server/telemetry/queries/utils.test.ts | 82 +++++++++++++++++++ .../cases/server/telemetry/queries/utils.ts | 10 ++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index 359aa621798f0..50b5b142fd319 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -565,6 +565,88 @@ describe('utils', () => { }); describe('files', () => { + it('rounds the average file size when it is a decimal', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + filesAggregations: { + averageSize: { value: 1.1 }, + topMimeTypes: { + buckets: [], + }, + }, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 1, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + + it('sets the average file size to 0 when the aggregation does not exist', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 0, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + it('sets the files stats to empty when the file aggregation results is the empty version', () => { const attachmentFramework: AttachmentFrameworkAggsResult = { externalReferenceTypes: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index 7896c2bdac760..e47e9d1613bce 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -230,7 +230,7 @@ export const getAttachmentsFrameworkStats = ({ return emptyAttachmentFramework(); } - const averageFileSize = filesAggregations?.averageSize?.value; + const averageFileSize = getAverageFileSize(filesAggregations); const topMimeTypes = filesAggregations?.topMimeTypes; return { @@ -253,6 +253,14 @@ export const getAttachmentsFrameworkStats = ({ }; }; +const getAverageFileSize = (filesAggregations?: FileAttachmentAggsResult) => { + if (filesAggregations?.averageSize?.value == null) { + return 0; + } + + return Math.round(filesAggregations.averageSize.value); +}; + const getAttachmentRegistryStats = ( registryResults: BucketsWithMaxOnCase, totalCasesForOwner: number From 755ddfe9cdfbf8df05f404b66997d0629ec3db97 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Mon, 24 Apr 2023 14:18:52 +0200 Subject: [PATCH 02/36] [Enterprise Search] Copyedit Elasticsearch, Search Applications (#155604) Minor copy clean up for clarity and concision --- .../components/elasticsearch_guide/elasticsearch_guide.tsx | 4 ++-- .../components/engines/engines_list.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx index 3820f8e334f07..7da4392ef1112 100644 --- a/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/elasticsearch/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -79,7 +79,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.elasticsearchDescription', { defaultMessage: - 'Whether you are building a search-powered application, or designing a large-scale search implementation, Elasticsearch provides the low-level tools to create the most relevant and performant search experience.', + "Elasticsearch provides the low-level tools you need to build fast, relevant search for your website or application. Because it's powerful and flexible, Elasticsearch can handle search use cases of all shapes and sizes.", } )}

@@ -103,7 +103,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.connectToElasticsearchDescription', { defaultMessage: - "Elastic builds and maintains clients in several popular languages and our community has contributed many more. They're easy to work with, feel natural to use, and, just like Elasticsearch, don't limit what you might want to do with them.", + 'Elastic builds and maintains clients in several popular languages and our community has contributed many more.', } )}

diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 48dce9f524164..7b93214e0af8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -166,7 +166,7 @@ export const EnginesList: React.FC = ({ createEngineFlyoutOpen }) => description: ( Date: Mon, 24 Apr 2023 08:20:44 -0400 Subject: [PATCH 03/36] [Synthetics] Improve toast information for add/update monitor (#155319) --- ...e_monitor_save.ts => use_monitor_save.tsx} | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) rename x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/{use_monitor_save.ts => use_monitor_save.tsx} (74%) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx similarity index 74% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx index 9fd18e5dfa04d..d8638c4b9ed92 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx @@ -6,8 +6,9 @@ */ import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public'; import { useParams, useRouteMatch } from 'react-router-dom'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; @@ -18,6 +19,8 @@ import { cleanMonitorListState } from '../../../state'; import { useSyntheticsRefreshContext } from '../../../contexts'; export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => { + const core = useKibana(); + const theme$ = core.services.theme?.theme$; const dispatch = useDispatch(); const { refreshApp } = useSyntheticsRefreshContext(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -51,10 +54,16 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito dispatch(cleanMonitorListState()); kibanaService.toasts.addSuccess({ title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, + text: toMountPoint( +

+ {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT : MONITOR_SUCCESS_LABEL_SUBTEXT} +

, + { theme$ } + ), toastLifeTimeMs: 3000, }); } - }, [data, status, monitorId, loading, refreshApp, dispatch]); + }, [data, status, monitorId, loading, refreshApp, dispatch, theme$]); return { status, loading, isEdit }; }; @@ -66,6 +75,13 @@ const MONITOR_SUCCESS_LABEL = i18n.translate( } ); +const MONITOR_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); + const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( 'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage', { @@ -79,3 +95,10 @@ const MONITOR_FAILURE_LABEL = i18n.translate( defaultMessage: 'Monitor was unable to be saved. Please try again later.', } ); + +const MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorFailureMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); From 9eee24f7bfc557d9aff48e30f3358d542ef5f476 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 24 Apr 2023 06:01:05 -0700 Subject: [PATCH 04/36] [Security Solution] Multi level grouping for alerts table (#152862) ## Multi Level Grouping Resolves https://github.com/elastic/kibana/issues/150516 Resolves https://github.com/elastic/kibana/issues/150514 Implements multi level grouping in Alerts table and Rule details table. Supports 3 levels deep. https://user-images.githubusercontent.com/6935300/232547389-7d778f69-d96d-4bd8-8560-f5ddd9fe8060.mov ### Test plan https://docs.google.com/document/d/15oseanNzF-u-Xeoahy1IVxI4oV3wOuO8VhA886cA1U8/edit# ### To do - [Cypress](https://github.com/elastic/kibana/issues/150666) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Steph Milovic --- .../steps/storybooks/build_and_upload.ts | 2 + .../.storybook/main.js | 9 + .../kbn-securitysolution-grouping/README.md | 3 - .../kbn-securitysolution-grouping/README.mdx | 3 + .../kbn-securitysolution-grouping/index.tsx | 8 +- .../accordion_panel/group_stats.test.tsx | 4 +- .../accordion_panel/group_stats.tsx | 31 +- .../components/accordion_panel/index.test.tsx | 1 + .../src/components/accordion_panel/index.tsx | 30 +- .../components/group_selector/index.test.tsx | 36 +- .../src/components/group_selector/index.tsx | 61 +- .../src/components/grouping.mock.tsx | 111 ++ .../src/components/grouping.stories.tsx | 28 + .../src/components/grouping.test.tsx | 95 +- .../src/components/grouping.tsx | 159 +- .../src/components/index.tsx | 5 +- .../src/components/styles.tsx | 37 +- .../src/components/translations.ts | 8 +- .../src/components/types.ts | 6 +- .../src/containers/query/index.ts | 2 +- .../src/containers/query/types.ts | 3 +- .../src/hooks/state/actions.ts | 53 +- .../src/hooks/state/reducer.test.ts | 39 +- .../src/hooks/state/reducer.ts | 48 +- .../src/hooks/types.ts | 32 +- .../src/hooks/use_get_group_selector.test.tsx | 115 +- .../src/hooks/use_get_group_selector.tsx | 116 +- .../src/hooks/use_grouping.test.tsx | 13 +- .../src/hooks/use_grouping.tsx | 80 +- .../src/hooks/use_grouping_pagination.ts | 53 - .../tsconfig.json | 3 +- src/dev/storybook/aliases.ts | 1 + .../alerts_treemap_panel/index.test.tsx | 11 - .../components/alerts_treemap_panel/index.tsx | 2 +- .../public/common/components/top_n/index.tsx | 2 +- .../alerts/use_alert_prevalence.test.ts | 46 - .../containers/alerts/use_alert_prevalence.ts | 2 +- .../containers/use_global_time/index.test.tsx | 74 +- .../containers/use_global_time/index.tsx | 22 +- .../public/common/store/grouping/actions.ts | 6 +- .../public/common/store/grouping/reducer.ts | 15 +- .../public/common/store/grouping/selectors.ts | 4 - .../public/common/store/grouping/types.ts | 1 - .../pages/rule_details/index.tsx | 2 +- .../alerts_count_panel/index.test.tsx | 13 - .../alerts_kpis/alerts_count_panel/index.tsx | 2 +- .../alerts_histogram_panel/index.tsx | 2 +- .../use_summary_chart_data.tsx | 2 +- .../alerts_table/alerts_grouping.test.tsx | 376 ++++ .../alerts_table/alerts_grouping.tsx | 341 ++-- .../alerts_table/alerts_sub_grouping.tsx | 259 +++ .../group_take_action_items.test.tsx | 119 +- .../group_take_action_items.tsx | 147 +- .../alerts_table/grouping_settings/mock.ts | 1736 +++++++++++++++++ .../grouping_settings/query_builder.ts | 6 +- .../components/alerts_table/index.test.tsx | 261 --- .../use_persistent_controls.tsx | 8 +- .../detection_engine.test.tsx | 159 +- .../detection_engine/detection_engine.tsx | 30 +- .../entity_analytics/anomalies/index.tsx | 2 +- .../entity_analytics/header/index.tsx | 2 +- 61 files changed, 3631 insertions(+), 1216 deletions(-) create mode 100644 packages/kbn-securitysolution-grouping/.storybook/main.js delete mode 100644 packages/kbn-securitysolution-grouping/README.md create mode 100644 packages/kbn-securitysolution-grouping/README.mdx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.mock.tsx create mode 100644 packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx delete mode 100644 packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts delete mode 100644 x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 949cb0a0ff534..b16e75abdb8a1 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -15,6 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'cases', + 'cell_actions', 'ci_composite', 'cloud_chat', 'coloring', @@ -34,6 +35,7 @@ const STORYBOOKS = [ 'expression_shape', 'expression_tagcloud', 'fleet', + 'grouping', 'home', 'infra', 'kibana_react', diff --git a/packages/kbn-securitysolution-grouping/.storybook/main.js b/packages/kbn-securitysolution-grouping/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/packages/kbn-securitysolution-grouping/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * 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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-securitysolution-grouping/README.md b/packages/kbn-securitysolution-grouping/README.md deleted file mode 100644 index 87b8047720a37..0000000000000 --- a/packages/kbn-securitysolution-grouping/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/securitysolution-grouping - -Grouping component and query. Currently only consumed by security solution alerts table. Package is a WIP. Refactoring to make generic https://github.com/elastic/kibana/issues/152491 diff --git a/packages/kbn-securitysolution-grouping/README.mdx b/packages/kbn-securitysolution-grouping/README.mdx new file mode 100644 index 0000000000000..b79cac381c298 --- /dev/null +++ b/packages/kbn-securitysolution-grouping/README.mdx @@ -0,0 +1,3 @@ +# @kbn/securitysolution-grouping + +Grouping component and query. Currently only consumed by security solution alerts table. diff --git a/packages/kbn-securitysolution-grouping/index.tsx b/packages/kbn-securitysolution-grouping/index.tsx index 92d69af316e1f..1b83c314714b7 100644 --- a/packages/kbn-securitysolution-grouping/index.tsx +++ b/packages/kbn-securitysolution-grouping/index.tsx @@ -6,20 +6,22 @@ * Side Public License, v 1. */ -import { RawBucket, StatRenderer, getGroupingQuery, isNoneGroup, useGrouping } from './src'; +import { getGroupingQuery, isNoneGroup, useGrouping } from './src'; import type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, + RawBucket, + StatRenderer, } from './src'; export { getGroupingQuery, isNoneGroup, useGrouping }; export type { + DynamicGroupingProps, GroupOption, GroupingAggregation, - GroupingFieldTotalAggregation, NamedAggregation, RawBucket, StatRenderer, diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx index 8df4c6ad6c7dc..8ccc1c912b62d 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx @@ -13,6 +13,8 @@ import { GroupStats } from './group_stats'; const onTakeActionsOpen = jest.fn(); const testProps = { bucketKey: '9nk5mo2fby', + groupFilter: [], + groupNumber: 0, onTakeActionsOpen, statRenderers: [ { @@ -23,7 +25,7 @@ const testProps = { { title: 'Rules:', badge: { value: 2 } }, { title: 'Alerts:', badge: { value: 2, width: 50, color: '#a83632' } }, ], - takeActionItems: [ + takeActionItems: () => [

,

, ], diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx index 00c6e7aa3a855..61f40982507b8 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx @@ -16,29 +16,44 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; +import { Filter } from '@kbn/es-query'; import { StatRenderer } from '../types'; import { statsContainerCss } from '../styles'; import { TAKE_ACTION } from '../translations'; interface GroupStatsProps { bucketKey: string; - statRenderers?: StatRenderer[]; + groupFilter: Filter[]; + groupNumber: number; onTakeActionsOpen?: () => void; - takeActionItems: JSX.Element[]; + statRenderers?: StatRenderer[]; + takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; } const GroupStatsComponent = ({ bucketKey, - statRenderers, + groupFilter, + groupNumber, onTakeActionsOpen, - takeActionItems, + statRenderers, + takeActionItems: getTakeActionItems, }: GroupStatsProps) => { const [isPopoverOpen, setPopover] = useState(false); + const [takeActionItems, setTakeActionItems] = useState([]); - const onButtonClick = useCallback( - () => (!isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen)), - [isPopoverOpen, onTakeActionsOpen] - ); + const onButtonClick = useCallback(() => { + if (!isPopoverOpen && takeActionItems.length === 0) { + setTakeActionItems(getTakeActionItems(groupFilter, groupNumber)); + } + return !isPopoverOpen && onTakeActionsOpen ? onTakeActionsOpen() : setPopover(!isPopoverOpen); + }, [ + getTakeActionItems, + groupFilter, + groupNumber, + isPopoverOpen, + onTakeActionsOpen, + takeActionItems.length, + ]); const statsComponent = useMemo( () => diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx index 9aa4b51437130..828e1059471e9 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx @@ -55,6 +55,7 @@ const testProps = { }, renderChildComponent, selectedGroup: 'kibana.alert.rule.name', + onGroupClose: () => {}, }; describe('grouping accordion panel', () => { diff --git a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx index 286cb18ffb6e6..c1d55495cf785 100644 --- a/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx @@ -8,7 +8,7 @@ import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { firstNonNullValue } from '../../helpers'; import type { RawBucket } from '../types'; import { createGroupFilter } from './helpers'; @@ -20,8 +20,9 @@ interface GroupPanelProps { forceState?: 'open' | 'closed'; groupBucket: RawBucket; groupPanelRenderer?: JSX.Element; + groupingLevel?: number; isLoading: boolean; - level?: number; + onGroupClose: () => void; onToggleGroup?: (isOpen: boolean, groupBucket: RawBucket) => void; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; selectedGroup: string; @@ -40,18 +41,30 @@ const DefaultGroupPanelRenderer = ({ title }: { title: string }) => ( ); const GroupPanelComponent = ({ - customAccordionButtonClassName = 'groupingAccordionForm__button', + customAccordionButtonClassName, customAccordionClassName = 'groupingAccordionForm', extraAction, forceState, groupBucket, groupPanelRenderer, + groupingLevel = 0, isLoading, - level = 0, + onGroupClose, onToggleGroup, renderChildComponent, selectedGroup, }: GroupPanelProps) => { + const lastForceState = useRef(forceState); + useEffect(() => { + if (lastForceState.current === 'open' && forceState === 'closed') { + // when parent group closes, reset pagination of any child groups + onGroupClose(); + lastForceState.current = 'closed'; + } else if (lastForceState.current === 'closed' && forceState === 'open') { + lastForceState.current = 'open'; + } + }, [onGroupClose, forceState, selectedGroup]); + const groupFieldValue = useMemo(() => firstNonNullValue(groupBucket.key), [groupBucket.key]); const groupFilters = useMemo( @@ -72,20 +85,21 @@ const GroupPanelComponent = ({ +

{groupPanelRenderer ?? }
} - className={customAccordionClassName} + buttonElement="div" + className={groupingLevel > 0 ? 'groupingAccordionFormLevel' : customAccordionClassName} data-test-subj="grouping-accordion" extraAction={extraAction} forceState={forceState} isLoading={isLoading} - id={`group${level}-${groupFieldValue}`} + id={`group${groupingLevel}-${groupFieldValue}`} onToggle={onToggle} paddingSize="m" > - {renderChildComponent(groupFilters)} + {renderChildComponent(groupFilters)} ); }; diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx index daa58396df70b..2172390f41e85 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx @@ -43,7 +43,7 @@ const testProps = { esTypes: ['ip'], }, ], - groupSelected: 'kibana.alert.rule.name', + groupsSelected: ['kibana.alert.rule.name'], onGroupChange, options: [ { @@ -90,4 +90,38 @@ describe('group selector', () => { fireEvent.click(getByTestId('panel-none')); expect(onGroupChange).toHaveBeenCalled(); }); + it('Labels button in correct selection order', () => { + const { getByTestId, rerender } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, User name, Host name'); + rerender( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name, User name'); + }); + it('Labels button with selection not in options', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); + it('Labels button when `none` is selected', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('group-selector-dropdown').title).toEqual('Rule name, Host name'); + }); }); diff --git a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx index f0274f7c73ab7..a2a876da97992 100644 --- a/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx @@ -21,21 +21,27 @@ export interface GroupSelectorProps { 'data-test-subj'?: string; fields: FieldSpec[]; groupingId: string; - groupSelected: string; + groupsSelected: string[]; onGroupChange: (groupSelection: string) => void; options: Array<{ key: string; label: string }>; title?: string; + maxGroupingLevels?: number; } - const GroupSelectorComponent = ({ 'data-test-subj': dataTestSubj, fields, - groupSelected = 'none', + groupsSelected = ['none'], onGroupChange, options, title = i18n.GROUP_BY, + maxGroupingLevels = 1, }: GroupSelectorProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const isGroupSelected = useCallback( + (groupKey: string) => + !!groupsSelected.find((selectedGroupKey) => selectedGroupKey === groupKey), + [groupsSelected] + ); const panels: EuiContextMenuPanelDescriptor[] = useMemo( () => [ @@ -49,7 +55,7 @@ const GroupSelectorComponent = ({ style={{ lineHeight: 1 }} > - {i18n.SELECT_FIELD.toUpperCase()} + {i18n.SELECT_FIELD(maxGroupingLevels)} onGroupChange('none'), }, ...options.map((o) => ({ 'data-test-subj': `panel-${o.key}`, + disabled: groupsSelected.length === maxGroupingLevels && !isGroupSelected(o.key), name: o.label, onClick: () => onGroupChange(o.key), - icon: groupSelected === o.key ? 'check' : 'empty', + icon: isGroupSelected(o.key) ? 'check' : 'empty', })), { 'data-test-subj': `panel-custom`, name: i18n.CUSTOM_FIELD, icon: 'empty', + disabled: groupsSelected.length === maxGroupingLevels, panel: 'customPanel', + hasPanel: true, }, ], }, @@ -91,24 +100,35 @@ const GroupSelectorComponent = ({ currentOptions={options.map((o) => ({ text: o.label, field: o.key }))} onSubmit={(field: string) => { onGroupChange(field); + setIsPopoverOpen(false); }} fields={fields} /> ), }, ], - [fields, groupSelected, onGroupChange, options] + [fields, groupsSelected.length, isGroupSelected, maxGroupingLevels, onGroupChange, options] ); - const selectedOption = useMemo( - () => options.filter((groupOption) => groupOption.key === groupSelected), - [groupSelected, options] + const selectedOptions = useMemo( + () => options.filter((groupOption) => isGroupSelected(groupOption.key)), + [isGroupSelected, options] ); const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []); const closePopover = useCallback(() => setIsPopoverOpen(false), []); - const button = useMemo( - () => ( + const button = useMemo(() => { + // need to use groupsSelected to ensure proper selection order (selectedOptions does not handle selection order) + const buttonLabel = isGroupSelected('none') + ? i18n.NONE + : groupsSelected.reduce((optionsTitle, o) => { + const selection = selectedOptions.find((opt) => opt.key === o); + if (selection == null) { + return optionsTitle; + } + return optionsTitle ? [optionsTitle, selection.label].join(', ') : selection.label; + }, ''); + return ( 0 - ? selectedOption[0].label - : i18n.NONE - } + title={buttonLabel} size="xs" > - {`${title}: ${ - groupSelected !== 'none' && selectedOption.length > 0 - ? selectedOption[0].label - : i18n.NONE - }`} + {`${title}: ${buttonLabel}`} - ), - [groupSelected, onButtonClick, selectedOption, title] - ); + ); + }, [groupsSelected, isGroupSelected, onButtonClick, selectedOptions, title]); return (

{'child component'}

, + onGroupClose: () => {}, + selectedGroup: 'kibana.alert.rule.name', + takeActionItems: () => [ + {}}> + {'Mark as acknowledged'} + , + {}}> + {'Mark as closed'} + , + ], + tracker: () => {}, +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx new file mode 100644 index 0000000000000..b961402ee3a7c --- /dev/null +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.stories.tsx @@ -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 type { Story } from '@storybook/react'; +import { mockGroupingProps } from './grouping.mock'; +import { Grouping } from './grouping'; +import readme from '../../README.mdx'; + +export default { + component: Grouping, + title: 'Grouping', + description: 'A group of accordion components that each renders a given child component', + parameters: { + docs: { + page: readme, + }, + }, +}; + +export const Emtpy: Story = () => { + return ; +}; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx index ff5a7d66a6042..2376614ab444c 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.test.tsx @@ -14,104 +14,15 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { METRIC_TYPE } from '@kbn/analytics'; import { getTelemetryEvent } from '../telemetry/const'; +import { mockGroupingProps, rule1Name, rule2Name } from './grouping.mock'; + const renderChildComponent = jest.fn(); const takeActionItems = jest.fn(); const mockTracker = jest.fn(); -const rule1Name = 'Rule 1 name'; -const rule1Desc = 'Rule 1 description'; -const rule2Name = 'Rule 2 name'; -const rule2Desc = 'Rule 2 description'; const testProps = { - data: { - groupsCount: { - value: 2, - }, - groupByFields: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: [rule1Name, rule1Desc], - key_as_string: `${rule1Name}|${rule1Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - alertsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - { - key: [rule2Name, rule2Desc], - key_as_string: `${rule2Name}|${rule2Desc}`, - doc_count: 1, - hostsCountAggregation: { - value: 1, - }, - ruleTags: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - unitsCount: { - value: 1, - }, - severitiesSubAggregation: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'low', - doc_count: 1, - }, - ], - }, - countSeveritySubAggregation: { - value: 1, - }, - usersCountAggregation: { - value: 1, - }, - }, - ], - }, - unitsCount: { - value: 2, - }, - }, - groupingId: 'test-grouping-id', - isLoading: false, - pagination: { - pageIndex: 0, - pageSize: 25, - onChangeItemsPerPage: jest.fn(), - onChangePage: jest.fn(), - itemsPerPageOptions: [10, 25, 50, 100], - }, + ...mockGroupingProps, renderChildComponent, - selectedGroup: 'kibana.alert.rule.name', takeActionItems, tracker: mockTracker, }; diff --git a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx index d77f1fe1a8106..625beda320d04 100644 --- a/packages/kbn-securitysolution-grouping/src/components/grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx @@ -21,35 +21,29 @@ import { createGroupFilter } from './accordion_panel/helpers'; import { GroupPanel } from './accordion_panel'; import { GroupStats } from './accordion_panel/group_stats'; import { EmptyGroupingComponent } from './empty_results_panel'; -import { groupingContainerCss, countCss } from './styles'; +import { countCss, groupingContainerCss, groupingContainerCssLevel } from './styles'; import { GROUPS_UNIT } from './translations'; -import type { - GroupingAggregation, - GroupingFieldTotalAggregation, - GroupPanelRenderer, - RawBucket, -} from './types'; -import { getTelemetryEvent } from '../telemetry/const'; +import type { GroupingAggregation, GroupPanelRenderer } from './types'; import { GroupStatsRenderer, OnGroupToggle } from './types'; +import { getTelemetryEvent } from '../telemetry/const'; export interface GroupingProps { - data?: GroupingAggregation & GroupingFieldTotalAggregation; - groupingId: string; + activePage: number; + data?: GroupingAggregation; groupPanelRenderer?: GroupPanelRenderer; groupSelector?: JSX.Element; // list of custom UI components which correspond to your custom rendered metrics aggregations groupStatsRenderer?: GroupStatsRenderer; + groupingId: string; + groupingLevel?: number; inspectButton?: JSX.Element; isLoading: boolean; + itemsPerPage: number; + onChangeGroupsItemsPerPage?: (size: number) => void; + onChangeGroupsPage?: (index: number) => void; onGroupToggle?: OnGroupToggle; - pagination: { - pageIndex: number; - pageSize: number; - onChangeItemsPerPage: (itemsPerPageNumber: number) => void; - onChangePage: (pageNumber: number) => void; - itemsPerPageOptions: number[]; - }; renderChildComponent: (groupFilter: Filter[]) => React.ReactElement; + onGroupClose: () => void; selectedGroup: string; takeActionItems: (groupFilters: Filter[], groupNumber: number) => JSX.Element[]; tracker?: ( @@ -61,24 +55,29 @@ export interface GroupingProps { } const GroupingComponent = ({ + activePage, data, - groupingId, groupPanelRenderer, groupSelector, groupStatsRenderer, + groupingId, + groupingLevel = 0, inspectButton, isLoading, + itemsPerPage, + onChangeGroupsItemsPerPage, + onChangeGroupsPage, + onGroupClose, onGroupToggle, - pagination, renderChildComponent, selectedGroup, takeActionItems, tracker, unit = defaultUnit, }: GroupingProps) => { - const [trigger, setTrigger] = useState< - Record }> - >({}); + const [trigger, setTrigger] = useState>( + {} + ); const unitCount = data?.unitsCount?.value ?? 0; const unitCountText = useMemo(() => { @@ -100,16 +99,16 @@ const GroupingComponent = ({ return ( } forceState={(trigger[groupKey] && trigger[groupKey].state) ?? 'closed'} @@ -128,7 +127,6 @@ const GroupingComponent = ({ // ...trigger, -> this change will keep only one group at a time expanded and one table displayed [groupKey]: { state: isOpen ? 'open' : 'closed', - selectedBucket: groupBucket, }, }); onGroupToggle?.({ isOpen, groupName: group, groupNumber, groupingId }); @@ -139,8 +137,9 @@ const GroupingComponent = ({ : () => } selectedGroup={selectedGroup} + groupingLevel={groupingLevel} /> - + {groupingLevel > 0 ? null : } ); }), @@ -149,7 +148,9 @@ const GroupingComponent = ({ groupPanelRenderer, groupStatsRenderer, groupingId, + groupingLevel, isLoading, + onGroupClose, onGroupToggle, renderChildComponent, selectedGroup, @@ -159,58 +160,76 @@ const GroupingComponent = ({ ] ); const pageCount = useMemo( - () => (groupCount && pagination.pageSize ? Math.ceil(groupCount / pagination.pageSize) : 1), - [groupCount, pagination.pageSize] + () => (groupCount ? Math.ceil(groupCount / itemsPerPage) : 1), + [groupCount, itemsPerPage] ); + return ( <> - - - {groupCount > 0 && unitCount > 0 ? ( - - - - {unitCountText} - - - - - {groupCountText} - - + {groupingLevel > 0 ? null : ( + + + {groupCount > 0 && unitCount > 0 ? ( + + + + {unitCountText} + + + + + {groupCountText} + + + + ) : null} + + + + {inspectButton && {inspectButton}} + {groupSelector} - ) : null} - - - - {inspectButton && {inspectButton}} - {groupSelector} - - - -
+ + + )} +
0 ? groupingContainerCssLevel : groupingContainerCss} + className="eui-xScroll" + > {isLoading && ( )} {groupCount > 0 ? ( <> {groupPanels} - - + {groupCount > 0 && ( + <> + + { + if (onChangeGroupsItemsPerPage) { + onChangeGroupsItemsPerPage(pageSize); + } + }} + onChangePage={(pageIndex: number) => { + if (onChangeGroupsPage) { + onChangeGroupsPage(pageIndex); + } + }} + pageCount={pageCount} + showPerPageOptions + /> + + )} ) : ( diff --git a/packages/kbn-securitysolution-grouping/src/components/index.tsx b/packages/kbn-securitysolution-grouping/src/components/index.tsx index c924da988b04e..0d759c0be48be 100644 --- a/packages/kbn-securitysolution-grouping/src/components/index.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/index.tsx @@ -14,8 +14,9 @@ export * from './grouping'; /** * Checks if no group is selected - * @param groupKey selected group field value + * @param groupKeys selected group field values * * @returns {boolean} True if no group is selected */ -export const isNoneGroup = (groupKey: string | null) => groupKey === NONE_GROUP_KEY; +export const isNoneGroup = (groupKeys: string[]) => + !!groupKeys.find((groupKey) => groupKey === NONE_GROUP_KEY); diff --git a/packages/kbn-securitysolution-grouping/src/components/styles.tsx b/packages/kbn-securitysolution-grouping/src/components/styles.tsx index abf3d83db508e..fcd2e4a61aaee 100644 --- a/packages/kbn-securitysolution-grouping/src/components/styles.tsx +++ b/packages/kbn-securitysolution-grouping/src/components/styles.tsx @@ -36,7 +36,7 @@ export const statsContainerCss = css` `; export const groupingContainerCss = css` - .euiAccordion__childWrapper .euiAccordion__padding--m { + .groupingAccordionForm .euiAccordion__childWrapper .euiAccordion__padding--m { margin-left: 8px; margin-right: 8px; border-left: ${euiThemeVars.euiBorderThin}; @@ -44,7 +44,7 @@ export const groupingContainerCss = css` border-bottom: ${euiThemeVars.euiBorderThin}; border-radius: 0 0 6px 6px; } - .euiAccordion__triggerWrapper { + .groupingAccordionForm .euiAccordion__triggerWrapper { border-bottom: ${euiThemeVars.euiBorderThin}; border-left: ${euiThemeVars.euiBorderThin}; border-right: ${euiThemeVars.euiBorderThin}; @@ -59,8 +59,37 @@ export const groupingContainerCss = css` border-radius: 6px; min-width: 1090px; } - .groupingAccordionForm__button { - text-decoration: none !important; + .groupingPanelRenderer { + display: table; + table-layout: fixed; + width: 100%; + padding-right: 32px; + } +`; + +export const groupingContainerCssLevel = css` + .groupingAccordionFormLevel .euiAccordion__childWrapper .euiAccordion__padding--m { + margin-left: 8px; + margin-right: 8px; + border-left: none; + border-right: none; + border-bottom: ${euiThemeVars.euiBorderThin}; + border-radius: 0; + } + .groupingAccordionFormLevel .euiAccordion__triggerWrapper { + border-bottom: ${euiThemeVars.euiBorderThin}; + border-left: none; + border-right: none; + min-height: 78px; + padding-left: 16px; + padding-right: 16px; + border-radius: 0; + } + .groupingAccordionFormLevel { + border-top: none; + border-bottom: none; + border-radius: 0; + min-width: 1090px; } .groupingPanelRenderer { display: table; diff --git a/packages/kbn-securitysolution-grouping/src/components/translations.ts b/packages/kbn-securitysolution-grouping/src/components/translations.ts index e3896d25b910f..f2cb8a172dbdc 100644 --- a/packages/kbn-securitysolution-grouping/src/components/translations.ts +++ b/packages/kbn-securitysolution-grouping/src/components/translations.ts @@ -35,9 +35,11 @@ export const GROUP_BY_CUSTOM_FIELD = i18n.translate('grouping.customGroupByPanel defaultMessage: 'Group By Custom Field', }); -export const SELECT_FIELD = i18n.translate('grouping.groupByPanelTitle', { - defaultMessage: 'Select Field', -}); +export const SELECT_FIELD = (groupingLevelsCount: number) => + i18n.translate('grouping.groupByPanelTitle', { + values: { groupingLevelsCount }, + defaultMessage: 'Select up to {groupingLevelsCount} groupings', + }); export const NONE = i18n.translate('grouping.noneGroupByOptionName', { defaultMessage: 'None', diff --git a/packages/kbn-securitysolution-grouping/src/components/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts index 8956056581cc6..cf5f55f5c27f3 100644 --- a/packages/kbn-securitysolution-grouping/src/components/types.ts +++ b/packages/kbn-securitysolution-grouping/src/components/types.ts @@ -19,7 +19,7 @@ export type RawBucket = GenericBuckets & T; /** Defines the shape of the aggregation returned by Elasticsearch */ // TODO: write developer docs for these fields -export interface GroupingAggregation { +export interface RootAggregation { groupByFields?: { buckets?: Array>; }; @@ -39,6 +39,8 @@ export type GroupingFieldTotalAggregation = Record< } >; +export type GroupingAggregation = RootAggregation & GroupingFieldTotalAggregation; + export interface BadgeMetric { value: number; color?: string; @@ -67,3 +69,5 @@ export type OnGroupToggle = (params: { groupNumber: number; groupingId: string; }) => void; + +export type { GroupingProps } from './grouping'; diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts index 986788bf0dfa0..23699c1ccf94a 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/index.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts @@ -35,10 +35,10 @@ export const getGroupingQuery = ({ additionalFilters = [], from, groupByFields, - pageNumber, rootAggregations, runtimeMappings, size = DEFAULT_GROUP_BY_FIELD_SIZE, + pageNumber, sort, statsAggregations, to, diff --git a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts index c56e26223550a..5a8b2f822fb5c 100644 --- a/packages/kbn-securitysolution-grouping/src/containers/query/types.ts +++ b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts @@ -24,9 +24,10 @@ export interface GroupingQueryArgs { additionalFilters: BoolAgg[]; from: string; groupByFields: string[]; - pageNumber?: number; rootAggregations?: NamedAggregation[]; runtimeMappings?: MappingRuntimeFields; + additionalAggregationsRoot?: NamedAggregation[]; + pageNumber?: number; size?: number; sort?: Array<{ [category: string]: { order: 'asc' | 'desc' } }>; statsAggregations?: NamedAggregation[]; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts index 0953e6872ee91..e1bdb08500fa8 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/actions.ts @@ -6,55 +6,20 @@ * Side Public License, v 1. */ -import { - ActionType, - GroupOption, - UpdateActiveGroup, - UpdateGroupActivePage, - UpdateGroupItemsPerPage, - UpdateGroupOptions, -} from '../types'; +import { ActionType, GroupOption, UpdateActiveGroups, UpdateGroupOptions } from '../types'; -const updateActiveGroup = ({ - activeGroup, +const updateActiveGroups = ({ + activeGroups, id, }: { - activeGroup: string; + activeGroups: string[]; id: string; -}): UpdateActiveGroup => ({ +}): UpdateActiveGroups => ({ payload: { - activeGroup, + activeGroups, id, }, - type: ActionType.updateActiveGroup, -}); - -const updateGroupActivePage = ({ - activePage, - id, -}: { - activePage: number; - id: string; -}): UpdateGroupActivePage => ({ - payload: { - activePage, - id, - }, - type: ActionType.updateGroupActivePage, -}); - -const updateGroupItemsPerPage = ({ - itemsPerPage, - id, -}: { - itemsPerPage: number; - id: string; -}): UpdateGroupItemsPerPage => ({ - payload: { - itemsPerPage, - id, - }, - type: ActionType.updateGroupItemsPerPage, + type: ActionType.updateActiveGroups, }); const updateGroupOptions = ({ @@ -72,8 +37,6 @@ const updateGroupOptions = ({ }); export const groupActions = { - updateActiveGroup, - updateGroupActivePage, - updateGroupItemsPerPage, + updateActiveGroups, updateGroupOptions, }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts index 5348731d39128..5a1b4112df3aa 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.test.ts @@ -24,7 +24,7 @@ const groupById = { [groupingId]: { ...defaultGroup, options: groupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; @@ -54,7 +54,7 @@ describe('grouping reducer', () => { JSON.stringify(groupingState.groupById) ); }); - it('updateActiveGroup', () => { + it('updateActiveGroups', () => { const { result } = renderHook(() => useReducer(groupsReducerWithStorage, { ...initialState, @@ -62,40 +62,11 @@ describe('grouping reducer', () => { }) ); let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('host.name'); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['host.name']); act(() => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup: 'user.name' })); + dispatch(groupActions.updateActiveGroups({ id: groupingId, activeGroups: ['user.name'] })); }); [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activeGroup).toEqual('user.name'); - }); - it('updateGroupActivePage', () => { - const { result } = renderHook(() => - useReducer(groupsReducerWithStorage, { - ...initialState, - groupById, - }) - ); - let [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(0); - act(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].activePage).toEqual(12); - }); - it('updateGroupItemsPerPage', () => { - const { result } = renderHook(() => useReducer(groupsReducerWithStorage, initialState)); - let [groupingState, dispatch] = result.current; - act(() => { - dispatch(groupActions.updateGroupOptions({ id: groupingId, newOptionList: groupingOptions })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(25); - act(() => { - dispatch(groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: 12 })); - }); - [groupingState, dispatch] = result.current; - expect(groupingState.groupById[groupingId].itemsPerPage).toEqual(12); + expect(groupingState.groupById[groupingId].activeGroups).toEqual(['user.name']); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts index 287227b5763b3..d59637b69defe 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/state/reducer.ts @@ -25,8 +25,8 @@ export const initialState: GroupMap = { const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) => { switch (action.type) { - case ActionType.updateActiveGroup: { - const { id, activeGroup } = action.payload; + case ActionType.updateActiveGroups: { + const { id, activeGroups } = action.payload; return { ...state, groupById: { @@ -34,35 +34,7 @@ const groupsReducer = (state: GroupMap, action: Action, groupsById: GroupsById) [id]: { ...defaultGroup, ...groupsById[id], - activeGroup, - }, - }, - }; - } - case ActionType.updateGroupActivePage: { - const { id, activePage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - activePage, - }, - }, - }; - } - case ActionType.updateGroupItemsPerPage: { - const { id, itemsPerPage } = action.payload; - return { - ...state, - groupById: { - ...groupsById, - [id]: { - ...defaultGroup, - ...groupsById[id], - itemsPerPage, + activeGroups, }, }, }; @@ -89,22 +61,10 @@ export const groupsReducerWithStorage = (state: GroupMap, action: Action) => { if (storage) { groupsInStorage = getAllGroupsInStorage(storage); } - const trackedGroupIds = Object.keys(state.groupById); - - const adjustedStorageGroups = Object.entries(groupsInStorage).reduce( - (acc: GroupsById, [key, group]) => ({ - ...acc, - [key]: { - // reset page to 0 if is initial state - ...(trackedGroupIds.includes(key) ? group : { ...group, activePage: 0 }), - }, - }), - {} as GroupsById - ); const groupsById: GroupsById = { ...state.groupById, - ...adjustedStorageGroups, + ...groupsInStorage, }; const newState = groupsReducer(state, action, groupsById); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/types.ts b/packages/kbn-securitysolution-grouping/src/hooks/types.ts index 4b5480794b30d..5c3e85d211eaf 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/types.ts +++ b/packages/kbn-securitysolution-grouping/src/hooks/types.ts @@ -8,35 +8,21 @@ // action types export enum ActionType { - updateActiveGroup = 'UPDATE_ACTIVE_GROUP', - updateGroupActivePage = 'UPDATE_GROUP_ACTIVE_PAGE', - updateGroupItemsPerPage = 'UPDATE_GROUP_ITEMS_PER_PAGE', + updateActiveGroups = 'UPDATE_ACTIVE_GROUPS', updateGroupOptions = 'UPDATE_GROUP_OPTIONS', } -export interface UpdateActiveGroup { - type: ActionType.updateActiveGroup; - payload: { activeGroup: string; id: string }; +export interface UpdateActiveGroups { + type: ActionType.updateActiveGroups; + payload: { activeGroups: string[]; id: string }; } -export interface UpdateGroupActivePage { - type: ActionType.updateGroupActivePage; - payload: { activePage: number; id: string }; -} -export interface UpdateGroupItemsPerPage { - type: ActionType.updateGroupItemsPerPage; - payload: { itemsPerPage: number; id: string }; -} export interface UpdateGroupOptions { type: ActionType.updateGroupOptions; payload: { newOptionList: GroupOption[]; id: string }; } -export type Action = - | UpdateActiveGroup - | UpdateGroupActivePage - | UpdateGroupItemsPerPage - | UpdateGroupOptions; +export type Action = UpdateActiveGroups | UpdateGroupOptions; // state @@ -46,10 +32,8 @@ export interface GroupOption { } export interface GroupModel { - activeGroup: string; + activeGroups: string[]; options: GroupOption[]; - activePage: number; - itemsPerPage: number; } export interface GroupsById { @@ -73,8 +57,6 @@ export interface Storage { export const EMPTY_GROUP_BY_ID: GroupsById = {}; export const defaultGroup: GroupModel = { - activePage: 0, - itemsPerPage: 25, - activeGroup: 'none', + activeGroups: ['none'], options: [], }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx index d29313b36e518..d741e7d15e670 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.test.tsx @@ -52,7 +52,7 @@ describe('useGetGroupSelector', () => { useGetGroupSelector({ ...defaultArgs, groupingState: { - groupById: { [groupingId]: { ...defaultGroup, activeGroup: customField } }, + groupById: { [groupingId]: { ...defaultGroup, activeGroups: [customField] } }, }, }) ); @@ -72,12 +72,12 @@ describe('useGetGroupSelector', () => { }); }); - it('On group change, does nothing when set to prev selected group', () => { + it('On group change, removes selected group if already selected', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -89,15 +89,22 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange('host.name')); - expect(dispatch).toHaveBeenCalledTimes(0); + + expect(dispatch).toHaveBeenCalledWith({ + payload: { + id: groupingId, + activeGroups: ['none'], + }, + type: ActionType.updateActiveGroups, + }); }); - it('On group change, resets active page, sets active group, and leaves options alone', () => { + it('On group change to none, remove all previously selected groups', () => { const testGroup = { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name', 'user.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -108,22 +115,43 @@ describe('useGetGroupSelector', () => { }, }, }); - act(() => result.current.props.onGroupChange('user.name')); - expect(dispatch).toHaveBeenNthCalledWith(1, { + act(() => result.current.props.onGroupChange('none')); + + expect(dispatch).toHaveBeenCalledWith({ payload: { id: groupingId, - activePage: 0, + activeGroups: ['none'], }, - type: ActionType.updateGroupActivePage, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenNthCalledWith(2, { + }); + + it('On group change, resets active page, sets active group, and leaves options alone', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name'], + }, + }; + const { result } = renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + act(() => result.current.props.onGroupChange('user.name')); + + expect(dispatch).toHaveBeenNthCalledWith(1, { payload: { id: groupingId, - activeGroup: 'user.name', + activeGroups: ['host.name', 'user.name'], }, - type: ActionType.updateActiveGroup, + type: ActionType.updateActiveGroups, }); - expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenCalledTimes(1); }); it('On group change, sends telemetry', () => { @@ -131,7 +159,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -155,7 +183,7 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; const { result } = renderHook((props) => useGetGroupSelector(props), { @@ -179,10 +207,10 @@ describe('useGetGroupSelector', () => { [groupingId]: { ...defaultGroup, options: defaultGroupingOptions, - activeGroup: 'host.name', + activeGroups: ['host.name'], }, }; - const { result } = renderHook((props) => useGetGroupSelector(props), { + const { result, rerender } = renderHook((props) => useGetGroupSelector(props), { initialProps: { ...defaultArgs, groupingState: { @@ -191,17 +219,54 @@ describe('useGetGroupSelector', () => { }, }); act(() => result.current.props.onGroupChange(customField)); - expect(dispatch).toHaveBeenCalledTimes(3); - expect(dispatch).toHaveBeenNthCalledWith(3, { + expect(dispatch).toHaveBeenCalledTimes(1); + rerender({ + ...defaultArgs, + groupingState: { + groupById: { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField], + }, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(2, { + payload: { + newOptionList: [...defaultGroupingOptions, { label: customField, key: customField }], + id: 'test-table', + }, + type: ActionType.updateGroupOptions, + }); + }); + + it('Supports multiple custom fields on initial load', () => { + const testGroup = { + [groupingId]: { + ...defaultGroup, + options: defaultGroupingOptions, + activeGroups: ['host.name', customField, 'another.custom'], + }, + }; + renderHook((props) => useGetGroupSelector(props), { + initialProps: { + ...defaultArgs, + groupingState: { + groupById: testGroup, + }, + }, + }); + expect(dispatch).toHaveBeenCalledTimes(1); + expect(dispatch).toHaveBeenCalledWith({ payload: { - id: groupingId, newOptionList: [ ...defaultGroupingOptions, - { - label: customField, - key: customField, - }, + { label: customField, key: customField }, + { label: 'another.custom', key: 'another.custom' }, ], + id: 'test-table', }, type: ActionType.updateGroupOptions, }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx index e3b1c45b2733d..05920beb37a47 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_get_group_selector.tsx @@ -22,6 +22,7 @@ export interface UseGetGroupSelectorArgs { fields: FieldSpec[]; groupingId: string; groupingState: GroupMap; + maxGroupingLevels?: number; onGroupChange?: (param: { groupByField: string; tableId: string }) => void; tracker?: ( type: UiCounterMetricType, @@ -36,22 +37,21 @@ export const useGetGroupSelector = ({ fields, groupingId, groupingState, + maxGroupingLevels = 1, onGroupChange, tracker, }: UseGetGroupSelectorArgs) => { - const { activeGroup: selectedGroup, options } = + const { activeGroups: selectedGroups, options } = groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - const setGroupsActivePage = useCallback( - (activePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage })); - }, - [dispatch, groupingId] - ); - - const setSelectedGroup = useCallback( - (activeGroup: string) => { - dispatch(groupActions.updateActiveGroup({ id: groupingId, activeGroup })); + const setSelectedGroups = useCallback( + (activeGroups: string[]) => { + dispatch( + groupActions.updateActiveGroups({ + id: groupingId, + activeGroups, + }) + ); }, [dispatch, groupingId] ); @@ -65,11 +65,20 @@ export const useGetGroupSelector = ({ const onChange = useCallback( (groupSelection: string) => { - if (groupSelection === selectedGroup) { + if (selectedGroups.find((selected) => selected === groupSelection)) { + const groups = selectedGroups.filter((selectedGroup) => selectedGroup !== groupSelection); + if (groups.length === 0) { + setSelectedGroups(['none']); + } else { + setSelectedGroups(groups); + } return; } - setGroupsActivePage(0); - setSelectedGroup(groupSelection); + + const newSelectedGroups = isNoneGroup([groupSelection]) + ? [groupSelection] + : [...selectedGroups.filter((selectedGroup) => selectedGroup !== 'none'), groupSelection]; + setSelectedGroups(newSelectedGroups); // built-in telemetry: UI-counter tracker?.( @@ -78,62 +87,57 @@ export const useGetGroupSelector = ({ ); onGroupChange?.({ tableId: groupingId, groupByField: groupSelection }); - - // only update options if the new selection is a custom field - if ( - !isNoneGroup(groupSelection) && - !options.find((o: GroupOption) => o.key === groupSelection) - ) { - setOptions([ - ...defaultGroupingOptions, - { - label: groupSelection, - key: groupSelection, - }, - ]); - } }, - [ - defaultGroupingOptions, - groupingId, - onGroupChange, - options, - selectedGroup, - setGroupsActivePage, - setOptions, - setSelectedGroup, - tracker, - ] + [groupingId, onGroupChange, selectedGroups, setSelectedGroups, tracker] ); useEffect(() => { - // only set options the first time, all other updates will be taken care of by onGroupChange - if (options.length > 0) return; - setOptions( - defaultGroupingOptions.find((o) => o.key === selectedGroup) - ? defaultGroupingOptions - : [ - ...defaultGroupingOptions, - ...(!isNoneGroup(selectedGroup) - ? [ - { + if (options.length === 0) { + return setOptions( + defaultGroupingOptions.find((o) => selectedGroups.find((selected) => selected === o.key)) + ? defaultGroupingOptions + : [ + ...defaultGroupingOptions, + ...(!isNoneGroup(selectedGroups) + ? selectedGroups.map((selectedGroup) => ({ key: selectedGroup, label: selectedGroup, - }, - ] - : []), - ] - ); - }, [defaultGroupingOptions, options.length, selectedGroup, setOptions]); + })) + : []), + ] + ); + } + if (isNoneGroup(selectedGroups)) { + return; + } + + const currentOptionKeys = options.map((o) => o.key); + const newOptions = [...options]; + selectedGroups.forEach((groupSelection) => { + if (currentOptionKeys.includes(groupSelection)) { + return; + } + // these are custom fields + newOptions.push({ + label: groupSelection, + key: groupSelection, + }); + }); + + if (newOptions.length !== options.length) { + setOptions(newOptions); + } + }, [defaultGroupingOptions, options, selectedGroups, setOptions]); return ( diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx index a2a9eeec8bf20..d95ae866d2ee9 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.test.tsx @@ -30,7 +30,6 @@ const defaultArgs = { groupStatsRenderer: jest.fn(), inspectButton: <>, onGroupToggle: jest.fn(), - renderChildComponent: () =>

{'hello'}

, }, }; @@ -38,6 +37,9 @@ const groupingArgs = { data: {}, isLoading: false, takeActionItems: jest.fn(), + activePage: 0, + itemsPerPage: 25, + onGroupClose: () => {}, }; describe('useGrouping', () => { @@ -70,6 +72,8 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: () =>

{'hello'}

, + selectedGroup: 'none', })} ); @@ -84,7 +88,7 @@ describe('useGrouping', () => { getItem.mockReturnValue( JSON.stringify({ 'test-table': { - activePage: 0, + itemsPerPageOptions: [10, 25, 50, 100], itemsPerPage: 25, activeGroup: 'kibana.alert.rule.name', options: defaultGroupingOptions, @@ -95,7 +99,7 @@ describe('useGrouping', () => { const { result, waitForNextUpdate } = renderHook(() => useGrouping(defaultArgs)); await waitForNextUpdate(); await waitForNextUpdate(); - const { getByTestId, queryByTestId } = render( + const { getByTestId } = render( {result.current.getGrouping({ ...groupingArgs, @@ -119,12 +123,13 @@ describe('useGrouping', () => { value: 18, }, }, + renderChildComponent: jest.fn(), + selectedGroup: 'test', })} ); expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(queryByTestId('innerTable')).not.toBeInTheDocument(); }); }); }); diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx index 993809943252f..5833ae8205d59 100644 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx +++ b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx @@ -11,8 +11,7 @@ import React, { useCallback, useMemo, useReducer } from 'react'; import { UiCounterMetricType } from '@kbn/analytics'; import { groupsReducerWithStorage, initialState } from './state/reducer'; import { GroupingProps, GroupSelectorProps, isNoneGroup } from '..'; -import { useGroupingPagination } from './use_grouping_pagination'; -import { groupActions, groupByIdSelector } from './state'; +import { groupByIdSelector } from './state'; import { useGetGroupSelector } from './use_get_group_selector'; import { defaultGroup, GroupOption } from './types'; import { Grouping as GroupingComponent } from '../components/grouping'; @@ -23,33 +22,37 @@ import { Grouping as GroupingComponent } from '../components/grouping'; interface Grouping { getGrouping: (props: DynamicGroupingProps) => React.ReactElement; groupSelector: React.ReactElement; - pagination: { - reset: () => void; - pageIndex: number; - pageSize: number; - }; - selectedGroup: string; + selectedGroups: string[]; } -/** Type for static grouping component props where T is the `GroupingAggregation` +/** Type for static grouping component props where T is the consumer `GroupingAggregation` * @interface StaticGroupingProps */ type StaticGroupingProps = Pick< GroupingProps, - | 'groupPanelRenderer' - | 'groupStatsRenderer' - | 'inspectButton' - | 'onGroupToggle' - | 'renderChildComponent' - | 'unit' + 'groupPanelRenderer' | 'groupStatsRenderer' | 'onGroupToggle' | 'unit' >; -/** Type for dynamic grouping component props where T is the `GroupingAggregation` +/** Type for dynamic grouping component props where T is the consumer `GroupingAggregation` * @interface DynamicGroupingProps */ -type DynamicGroupingProps = Pick, 'data' | 'isLoading' | 'takeActionItems'>; +export type DynamicGroupingProps = Pick< + GroupingProps, + | 'activePage' + | 'data' + | 'groupingLevel' + | 'inspectButton' + | 'isLoading' + | 'itemsPerPage' + | 'onChangeGroupsItemsPerPage' + | 'onChangeGroupsPage' + | 'renderChildComponent' + | 'onGroupClose' + | 'selectedGroup' + | 'takeActionItems' +>; -/** Interface for configuring grouping package where T is the `GroupingAggregation` +/** Interface for configuring grouping package where T is the consumer `GroupingAggregation` * @interface GroupingArgs */ interface GroupingArgs { @@ -57,6 +60,7 @@ interface GroupingArgs { defaultGroupingOptions: GroupOption[]; fields: FieldSpec[]; groupingId: string; + maxGroupingLevels?: number; /** for tracking * @param param { groupByField: string; tableId: string } selected group and table id */ @@ -75,21 +79,22 @@ interface GroupingArgs { * @param defaultGroupingOptions defines the grouping options as an array of {@link GroupOption} * @param fields FieldSpec array serialized version of DataViewField fields. Available in the custom grouping options * @param groupingId Unique identifier of the grouping component. Used in local storage + * @param maxGroupingLevels maximum group nesting levels (optional) * @param onGroupChange callback executed when selected group is changed, used for tracking * @param tracker telemetry handler - * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroup } + * @returns {@link Grouping} the grouping constructor { getGrouping, groupSelector, pagination, selectedGroups } */ export const useGrouping = ({ componentProps, defaultGroupingOptions, fields, groupingId, + maxGroupingLevels, onGroupChange, tracker, }: GroupingArgs): Grouping => { const [groupingState, dispatch] = useReducer(groupsReducerWithStorage, initialState); - - const { activeGroup: selectedGroup } = useMemo( + const { activeGroups: selectedGroups } = useMemo( () => groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup, [groupingId, groupingState] ); @@ -100,56 +105,37 @@ export const useGrouping = ({ fields, groupingId, groupingState, + maxGroupingLevels, onGroupChange, tracker, }); - const pagination = useGroupingPagination({ groupingId, groupingState, dispatch }); - const getGrouping = useCallback( /** * * @param props {@link DynamicGroupingProps} */ (props: DynamicGroupingProps): React.ReactElement => - isNoneGroup(selectedGroup) ? ( - componentProps.renderChildComponent([]) + isNoneGroup([props.selectedGroup]) ? ( + props.renderChildComponent([]) ) : ( ), - [componentProps, groupSelector, groupingId, pagination, selectedGroup, tracker] + [componentProps, groupSelector, groupingId, tracker] ); - const resetPagination = useCallback(() => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: 0 })); - }, [groupingId]); - return useMemo( () => ({ getGrouping, groupSelector, - selectedGroup, - pagination: { - reset: resetPagination, - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - }, + selectedGroups, }), - [ - getGrouping, - groupSelector, - pagination.pageIndex, - pagination.pageSize, - resetPagination, - selectedGroup, - ] + [getGrouping, groupSelector, selectedGroups] ); }; diff --git a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts b/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts deleted file mode 100644 index 9aa07458aaf6f..0000000000000 --- a/packages/kbn-securitysolution-grouping/src/hooks/use_grouping_pagination.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { useCallback, useMemo } from 'react'; -import { groupActions, groupByIdSelector } from './state'; -import { Action, defaultGroup, GroupMap } from './types'; - -export interface UseGroupingPaginationArgs { - dispatch: React.Dispatch; - groupingId: string; - groupingState: GroupMap; -} - -export const useGroupingPagination = ({ - groupingId, - groupingState, - dispatch, -}: UseGroupingPaginationArgs) => { - const { activePage, itemsPerPage } = - groupByIdSelector({ groups: groupingState }, groupingId) ?? defaultGroup; - - const setGroupsActivePage = useCallback( - (newActivePage: number) => { - dispatch(groupActions.updateGroupActivePage({ id: groupingId, activePage: newActivePage })); - }, - [dispatch, groupingId] - ); - - const setGroupsItemsPerPage = useCallback( - (newItemsPerPage: number) => { - dispatch( - groupActions.updateGroupItemsPerPage({ id: groupingId, itemsPerPage: newItemsPerPage }) - ); - }, - [dispatch, groupingId] - ); - - return useMemo( - () => ({ - pageIndex: activePage, - pageSize: itemsPerPage, - onChangeItemsPerPage: setGroupsItemsPerPage, - onChangePage: setGroupsActivePage, - itemsPerPageOptions: [10, 25, 50, 100], - }), - [activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage] - ); -}; diff --git a/packages/kbn-securitysolution-grouping/tsconfig.json b/packages/kbn-securitysolution-grouping/tsconfig.json index ab98ec47e3c93..621ba68957cf1 100644 --- a/packages/kbn-securitysolution-grouping/tsconfig.json +++ b/packages/kbn-securitysolution-grouping/tsconfig.json @@ -6,7 +6,8 @@ "jest", "node", "react", - "@emotion/react/types/css-prop" + "@emotion/react/types/css-prop", + "@kbn/ambient-ui-types" ] }, "include": [ diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index da762f8f55725..c612ef05d4b7a 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -37,6 +37,7 @@ export const storybookAliases = { expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', + grouping: 'packages/kbn-securitysolution-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx index 5a3f4b3e25e0e..07342c4f60691 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { SecurityPageName } from '../../../../common/constants'; -import { useGlobalTime } from '../../containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1, @@ -151,16 +150,6 @@ describe('AlertsTreemapPanel', () => { await waitFor(() => expect(screen.getByTestId('treemapPanel')).toBeInTheDocument()); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - render( - - - - ); - - await waitFor(() => expect(useGlobalTime).toBeCalledWith(false)); - }); - it('renders the panel with a hidden overflow-x', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx index a3ba582b3c974..33e526932c3e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx @@ -80,7 +80,7 @@ const AlertsTreemapPanelComponent: React.FC = ({ stackByWidth, title, }: Props) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${ALERTS_TREEMAP_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index e1b5f31f2d3b5..0113e0e584b54 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -110,7 +110,7 @@ const StatefulTopNComponent: React.FC = ({ value, }) => { const { uiSettings } = useKibana().services; - const { from, deleteQuery, setQuery, to } = useGlobalTime(false); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const options = getOptions(isActiveTimeline(scopeId ?? '') ? activeTimelineEventType : undefined); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts deleted file mode 100644 index b1a21d9ae492d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { renderHook } from '@testing-library/react-hooks'; - -import { TestProviders } from '../../mock'; -import { useAlertPrevalence } from './use_alert_prevalence'; -import { useGlobalTime } from '../use_global_time'; - -const from = '2022-07-28T08:20:18.966Z'; -const to = '2022-07-28T08:20:18.966Z'; -jest.mock('../use_global_time', () => { - const actual = jest.requireActual('../use_global_time'); - return { - ...actual, - useGlobalTime: jest - .fn() - .mockReturnValue({ from, to, setQuery: jest.fn(), deleteQuery: jest.fn() }), - }; -}); - -describe('useAlertPrevalence', () => { - beforeEach(() => jest.resetAllMocks()); - - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', () => { - renderHook( - () => - useAlertPrevalence({ - field: 'host.name', - value: ['Host-byc3w6qlpo'], - isActiveTimelines: false, - signalIndexName: null, - includeAlertIds: false, - }), - { - wrapper: TestProviders, - } - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts index 03ac3d6169351..3e98e067bfe2d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts @@ -44,7 +44,7 @@ export const useAlertPrevalence = ({ const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); - const globalTime = useGlobalTime(false); + const globalTime = useGlobalTime(); let to: string | undefined; let from: string | undefined; if (ignoreTimerange === false) { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx index 480ecdb3674ff..46a2738d6247a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -37,23 +37,77 @@ describe('useGlobalTime', () => { expect(result1.to).toBe(0); }); - test('clear all queries at unmount when clearAllQuery is set to true', () => { - const { unmount } = renderHook(() => useGlobalTime()); + test('clear query at unmount when setQuery has been called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/local/inputs/DELETE_ALL_QUERY' + expect(mockDispatch.mock.calls.length).toBe(2); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' ); }); - test('do NOT clear all queries at unmount when clearAllQuery is set to false.', () => { - const { unmount } = renderHook(() => useGlobalTime(false)); + test('do NOT clear query at unmount when setQuery has not been called', () => { + const { unmount } = renderHook(() => useGlobalTime()); unmount(); expect(mockDispatch.mock.calls.length).toBe(0); }); - test('do NOT clear all queries when setting state and clearAllQuery is set to true', () => { - const { rerender } = renderHook(() => useGlobalTime()); - act(() => rerender()); - expect(mockDispatch.mock.calls.length).toBe(0); + test('do clears only the dismounted queries at unmount when setQuery is called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + + act(() => { + result.current.setQuery({ + id: 'query-1', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + const { result: theOneWillNotBeDismounted } = renderHook(() => useGlobalTime()); + + act(() => { + theOneWillNotBeDismounted.current.setQuery({ + id: 'query-3h', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); + expect(mockDispatch).toHaveBeenCalledTimes(5); + expect(mockDispatch.mock.calls[3][0].payload.id).toEqual('query-1'); + + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); + + expect(mockDispatch.mock.calls[4][0].payload.id).toEqual('query-2'); + + expect(mockDispatch.mock.calls[4][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index dbb57d57c3e6e..76cd23c8efba0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -6,7 +6,7 @@ */ import { pick } from 'lodash/fp'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { InputsModelId } from '../../store/inputs/constants'; @@ -15,15 +15,18 @@ import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; import type { SetQuery, DeleteQuery } from './types'; -export const useGlobalTime = (clearAllQuery: boolean = true) => { +export const useGlobalTime = () => { const dispatch = useDispatch(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.globalTimeRangeSelector(state)) ); const [isInitializing, setIsInitializing] = useState(true); + const queryId = useRef([]); + const setQuery = useCallback( - ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => + ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => { + queryId.current = [...queryId.current, id]; dispatch( inputsActions.setQuery({ inputId: InputsModelId.global, @@ -33,7 +36,8 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { refetch, searchSessionId, }) - ), + ); + }, [dispatch] ); @@ -50,13 +54,13 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { // This effect must not have any mutable dependencies. Otherwise, the cleanup function gets called before the component unmounts. useEffect(() => { return () => { - if (clearAllQuery) { - dispatch(inputsActions.deleteAllQuery({ id: InputsModelId.global })); + if (queryId.current.length > 0) { + queryId.current.forEach((id) => deleteQuery({ id })); } }; - }, [dispatch, clearAllQuery]); + }, [deleteQuery]); - const memoizedReturn = useMemo( + return useMemo( () => ({ isInitializing, from, @@ -66,8 +70,6 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { }), [deleteQuery, from, isInitializing, setQuery, to] ); - - return memoizedReturn; }; export type GlobalTimeArgs = Omit, 'deleteQuery'> & diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts index a61186aeb0f8f..d78be8b03cb4d 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts @@ -11,9 +11,5 @@ import type React from 'react'; const actionCreator = actionCreatorFactory('x-pack/security_solution/groups'); export const updateGroupSelector = actionCreator<{ - groupSelector: React.ReactElement; + groupSelector: React.ReactElement | null; }>('UPDATE_GROUP_SELECTOR'); - -export const updateSelectedGroup = actionCreator<{ - selectedGroup: string; -}>('UPDATE_SELECTED_GROUP'); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts index aaea793e4ca86..6914e4ad465fe 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts @@ -6,20 +6,17 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { updateGroupSelector, updateSelectedGroup } from './actions'; +import { updateGroupSelector } from './actions'; import type { GroupModel } from './types'; export const initialGroupingState: GroupModel = { groupSelector: null, - selectedGroup: null, }; -export const groupsReducer = reducerWithInitialState(initialGroupingState) - .case(updateSelectedGroup, (state, { selectedGroup }) => ({ - ...state, - selectedGroup, - })) - .case(updateGroupSelector, (state, { groupSelector }) => ({ +export const groupsReducer = reducerWithInitialState(initialGroupingState).case( + updateGroupSelector, + (state, { groupSelector }) => ({ ...state, groupSelector, - })); + }) +); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts index eb63e256a4d9f..126fdac8c1b36 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts @@ -11,7 +11,3 @@ import type { GroupState } from './types'; const groupSelector = (state: GroupState) => state.groups.groupSelector; export const getGroupSelector = () => createSelector(groupSelector, (selector) => selector); - -export const selectedGroup = (state: GroupState) => state.groups.selectedGroup; - -export const getSelectedGroup = () => createSelector(selectedGroup, (group) => group); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts index 7d8fd4bc3eeca..d2250b15722ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts @@ -7,7 +7,6 @@ export interface GroupModel { groupSelector: React.ReactElement | null; - selectedGroup: string | null; } export interface GroupState { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index 6e1b4fddbd167..90f1d38f69774 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -852,7 +852,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleId != null && ( { }); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - await act(async () => { - mount( - - - - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); - }); - it('renders with the specified `alignHeader` alignment', async () => { await act(async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 26eb4522ed617..f9967a44ffb6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -84,7 +84,7 @@ export const AlertsCountPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); // create a unique, but stable (across re-renders) query id diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index d1cb85cdf4564..e1945ca151cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -152,7 +152,7 @@ export const AlertsHistogramPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx index fb5024d3c2e50..e8d0ddd061e81 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx @@ -82,7 +82,7 @@ export const useSummaryChartData: UseAlerts = ({ signalIndexName, skip = false, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx new file mode 100644 index 0000000000000..4a57d7cac8e73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx @@ -0,0 +1,376 @@ +/* + * 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 React from 'react'; +import { fireEvent, render, within } from '@testing-library/react'; +import type { Filter } from '@kbn/es-query'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import '../../../common/mock/match_media'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../../common/mock'; +import type { AlertsTableComponentProps } from './alerts_grouping'; +import { GroupedAlertsTable } from './alerts_grouping'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; +import { createStore } from '../../../common/store'; +import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { groupingSearchResponse } from './grouping_settings/mock'; + +jest.mock('../../containers/detection_engine/alerts/use_query'); +jest.mock('../../../common/containers/sourcerer'); +jest.mock('../../../common/utils/normalize_time_range'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), +})); + +const mockOptions = [ + { label: 'ruleName', key: 'kibana.alert.rule.name' }, + { label: 'userName', key: 'user.name' }, + { label: 'hostName', key: 'host.name' }, + { label: 'sourceIP', key: 'source.ip' }, +]; +// +jest.mock('./grouping_settings', () => { + const actual = jest.requireActual('./grouping_settings'); + + return { + ...actual, + getDefaultGroupingOptions: () => mockOptions, + }; +}); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockUseFieldBrowserOptions = jest.fn(); +jest.mock('../../../timelines/components/fields_browser', () => ({ + useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), +})); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); +const mockedUseKibana = mockUseKibana(); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ + useAddBulkToTimelineAction: jest.fn(() => {}), +})); +const sourcererDataView = { + indicesExist: true, + loading: false, + indexPattern: { + fields: [], + }, + browserFields: {}, +}; +const renderChildComponent = (groupingFilters: Filter[]) =>

; + +const testProps: AlertsTableComponentProps = { + defaultFilters: [], + from: '2020-07-07T08:20:18.966Z', + globalFilters: [], + globalQuery: { + query: 'query', + language: 'language', + }, + hasIndexMaintenance: true, + hasIndexWrite: true, + loading: false, + renderChildComponent, + runtimeMappings: {}, + signalIndexName: 'test', + tableId: TableId.test, + to: '2020-07-08T08:20:18.966Z', +}; + +const mockUseQueryAlerts = useQueryAlerts as jest.Mock; +const mockQueryResponse = { + loading: false, + data: {}, + setQuery: () => {}, + response: '', + request: '', + refetch: () => {}, +}; + +const getMockStorageState = (groups: string[] = ['none']) => + JSON.stringify({ + [testProps.tableId]: { + activeGroups: groups, + options: mockOptions, + }, + }); + +describe('GroupedAlertsTable', () => { + const { storage } = createSecuritySolutionStorageMock(); + let store: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + selectedPatterns: ['myFakebeat-*'], + }); + mockUseQueryAlerts.mockImplementation((i) => { + if (i.skip) { + return mockQueryResponse; + } + if (i.query.aggs.groupByFields.multi_terms != null) { + return { + ...mockQueryResponse, + data: groupingSearchResponse.ruleName, + }; + } + return { + ...mockQueryResponse, + data: i.query.aggs.groupByFields.terms.field != null ? groupingSearchResponse.hostName : {}, + }; + }); + }); + + it('calls the proper initial dispatch actions for groups', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument(); + expect(queryByTestId('group-selector-dropdown')).not.toBeInTheDocument(); + expect(getByTestId('alerts-table')).toBeInTheDocument(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' + ); + }); + + it('renders empty grouping table when group is selected without data', async () => { + mockUseQueryAlerts.mockReturnValue(mockQueryResponse); + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + const { getByTestId, queryByTestId } = render( + + + + ); + expect(queryByTestId('alerts-table')).not.toBeInTheDocument(); + expect(getByTestId('empty-results-panel')).toBeInTheDocument(); + }); + + it('renders grouping table in first accordion level when single group is selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('renders grouping table in second accordion level when 2 groups are selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).queryByTestId('alerts-table')).not.toBeInTheDocument(); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + const level1 = within(getAllByTestId('grouping-accordion-content')[1]); + expect(level1.getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('resets all levels pagination when selected group changes', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + fireEvent.click(getAllByTestId('group-selector-dropdown')[0]); + fireEvent.click(getAllByTestId('panel-user.name')[0]); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + // level 2 has been removed with the group selection change + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets all levels pagination when global query updates', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId, rerender } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + rerender( + + + + ); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets only most inner group pagination when its parent groups open/close', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[28]); + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-0') + .getAttribute('aria-current') + ).toEqual('true'); + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-1') + .getAttribute('aria-current') + ).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx index e5868970f6768..99ae3f4ff3433 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx @@ -5,50 +5,29 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import { useDispatch } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { buildEsQuery } from '@kbn/es-query'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import type { - GroupingFieldTotalAggregation, - GroupingAggregation, -} from '@kbn/securitysolution-grouping'; -import { useGrouping, isNoneGroup } from '@kbn/securitysolution-grouping'; +import type { GroupOption } from '@kbn/securitysolution-grouping'; +import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; +import { isEmpty, isEqual } from 'lodash/fp'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; -import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import { groupSelectors } from '../../../common/store/grouping'; +import type { State } from '../../../common/store'; +import { updateGroupSelector } from '../../../common/store/grouping/actions'; import type { Status } from '../../../../common/detection_engine/schemas/common'; -import { InspectButton } from '../../../common/components/inspect'; import { defaultUnit } from '../../../common/components/toolbar/unit'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { combineQueries } from '../../../common/lib/kuery'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { useKibana } from '../../../common/lib/kibana'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useInspectButton } from '../alerts_kpis/common/hooks'; - -import { buildTimeRangeFilter } from './helpers'; -import * as i18n from './translations'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; -import { - getAlertsGroupingQuery, - getDefaultGroupingOptions, - renderGroupPanel, - getStats, - useGroupTakeActionsItems, -} from './grouping_settings'; -import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions'; +import { getDefaultGroupingOptions, renderGroupPanel, getStats } from './grouping_settings'; +import { useKibana } from '../../../common/lib/kibana'; +import { GroupedSubLevel } from './alerts_sub_grouping'; import { track } from '../../../common/lib/telemetry'; -const ALERTS_GROUPING_ID = 'alerts-grouping'; - export interface AlertsTableComponentProps { - currentAlertStatusFilterValue?: Status; + currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; from: string; globalFilters: Filter[]; @@ -63,52 +42,37 @@ export interface AlertsTableComponentProps { to: string; } -export const GroupedAlertsTableComponent: React.FC = ({ - defaultFilters = [], - from, - globalFilters, - globalQuery, - hasIndexMaintenance, - hasIndexWrite, - loading, - tableId, - to, - runtimeMappings, - signalIndexName, - currentAlertStatusFilterValue, - renderChildComponent, -}) => { - const dispatch = useDispatch(); +const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_INDEX = 0; +const MAX_GROUPING_LEVELS = 3; - const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( - SourcererScopeName.detections +const useStorage = (storage: Storage, tableId: string) => + useMemo( + () => ({ + getStoragePageSize: (): number[] => { + const pageSizes = storage.get(`grouping-table-${tableId}`); + if (!pageSizes) { + return Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_SIZE); + } + return pageSizes; + }, + setStoragePageSize: (pageSizes: number[]) => { + storage.set(`grouping-table-${tableId}`, pageSizes); + }, + }), + [storage, tableId] ); + +const GroupedAlertsTableComponent: React.FC = (props) => { + const dispatch = useDispatch(); + + const { indexPattern, selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const { - services: { uiSettings, telemetry }, + services: { storage, telemetry }, } = useKibana(); - const getGlobalQuery = useCallback( - (customFilters: Filter[]) => { - if (browserFields != null && indexPattern != null) { - return combineQueries({ - config: getEsQueryConfig(uiSettings), - dataProviders: [], - indexPattern, - browserFields, - filters: [ - ...(defaultFilters ?? []), - ...globalFilters, - ...customFilters, - ...buildTimeRangeFilter(from, to), - ], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - }); - } - return null; - }, - [browserFields, indexPattern, uiSettings, defaultFilters, globalFilters, from, to, globalQuery] - ); + const { getStoragePageSize, setStoragePageSize } = useStorage(storage, props.tableId); const { onGroupChange, onGroupToggle } = useMemo( () => ({ @@ -125,153 +89,146 @@ export const GroupedAlertsTableComponent: React.FC = [telemetry] ); - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); - - const inspect = useMemo( - () => ( - - ), - [uniqueQueryId] - ); - - const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({ + const { groupSelector, getGrouping, selectedGroups } = useGrouping({ componentProps: { groupPanelRenderer: renderGroupPanel, groupStatsRenderer: getStats, - inspectButton: inspect, onGroupToggle, - renderChildComponent, unit: defaultUnit, }, - defaultGroupingOptions: getDefaultGroupingOptions(tableId), + defaultGroupingOptions: getDefaultGroupingOptions(props.tableId), fields: indexPattern.fields, - groupingId: tableId, + groupingId: props.tableId, + maxGroupingLevels: MAX_GROUPING_LEVELS, onGroupChange, tracker: track, }); - const resetPagination = pagination.reset; - useEffect(() => { - dispatch(updateGroupSelector({ groupSelector })); - }, [dispatch, groupSelector]); + const getGroupSelector = groupSelectors.getGroupSelector(); - useEffect(() => { - dispatch(updateSelectedGroup({ selectedGroup })); - }, [dispatch, selectedGroup]); - - useInvalidFilterQuery({ - id: tableId, - filterQuery: getGlobalQuery([])?.filterQuery, - kqlError: getGlobalQuery([])?.kqlError, - query: globalQuery, - startDate: from, - endDate: to, - }); + const groupSelectorInRedux = useSelector((state: State) => getGroupSelector(state)); + const selectorOptions = useRef([]); - const { deleteQuery, setQuery } = useGlobalTime(false); - const additionalFilters = useMemo(() => { - resetPagination(); - try { - return [ - buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ - ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), - ...(defaultFilters ?? []), - ]), - ]; - } catch (e) { - return []; + useEffect(() => { + if ( + isNoneGroup(selectedGroups) && + groupSelector.props.options.length > 0 && + (groupSelectorInRedux == null || + !isEqual(selectorOptions.current, groupSelector.props.options)) + ) { + selectorOptions.current = groupSelector.props.options; + dispatch(updateGroupSelector({ groupSelector })); + } else if (!isNoneGroup(selectedGroups) && groupSelectorInRedux !== null) { + dispatch(updateGroupSelector({ groupSelector: null })); } - }, [defaultFilters, globalFilters, globalQuery, resetPagination]); + }, [dispatch, groupSelector, groupSelectorInRedux, selectedGroups]); - const queryGroups = useMemo( - () => - getAlertsGroupingQuery({ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pageSize: pagination.pageSize, - pageIndex: pagination.pageIndex, - }), - [ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pagination.pageSize, - pagination.pageIndex, - ] + const [pageIndex, setPageIndex] = useState( + Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_INDEX) ); + const [pageSize, setPageSize] = useState(getStoragePageSize); - const { - data: alertsGroupsData, - loading: isLoadingGroups, - refetch, - request, - response, - setQuery: setAlertsQuery, - } = useQueryAlerts< - {}, - GroupingAggregation & - GroupingFieldTotalAggregation - >({ - query: queryGroups, - indexName: signalIndexName, - queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, - skip: isNoneGroup(selectedGroup), - }); + const resetAllPagination = useCallback(() => { + setPageIndex((curr) => curr.map(() => DEFAULT_PAGE_INDEX)); + }, []); useEffect(() => { - if (!isNoneGroup(selectedGroup)) { - setAlertsQuery(queryGroups); - } - }, [queryGroups, selectedGroup, setAlertsQuery]); + resetAllPagination(); + }, [resetAllPagination, selectedGroups]); + + const setPageVar = useCallback( + (newNumber: number, groupingLevel: number, pageType: 'index' | 'size') => { + if (pageType === 'index') { + setPageIndex((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + return newArr; + }); + } - useInspectButton({ - deleteQuery, - loading: isLoadingGroups, - response, - setQuery, - refetch, - request, - uniqueQueryId, - }); + if (pageType === 'size') { + setPageSize((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + setStoragePageSize(newArr); + return newArr; + }); + } + }, + [setStoragePageSize] + ); - const takeActionItems = useGroupTakeActionsItems({ - indexName: indexPattern.title, - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + const nonGroupingFilters = useRef({ + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, }); - const getTakeActionItems = useCallback( - (groupFilters: Filter[], groupNumber: number) => - takeActionItems({ - query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, - tableId, - groupNumber, - selectedGroup, - }), - [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] - ); + useEffect(() => { + const nonGrouping = { + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, + }; + if (!isEqual(nonGroupingFilters.current, nonGrouping)) { + resetAllPagination(); + nonGroupingFilters.current = nonGrouping; + } + }, [props.defaultFilters, props.globalFilters, props.globalQuery, resetAllPagination]); + + const getLevel = useCallback( + (level: number, selectedGroup: string, parentGroupingFilter?: string) => { + let rcc; + if (level < selectedGroups.length - 1) { + rcc = (groupingFilters: Filter[]) => { + return getLevel( + level + 1, + selectedGroups[level + 1], + JSON.stringify([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]) + ); + }; + } else { + rcc = (groupingFilters: Filter[]) => { + return props.renderChildComponent([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]); + }; + } - const groupedAlerts = useMemo( - () => - getGrouping({ - data: alertsGroupsData?.aggregations, - isLoading: loading || isLoadingGroups, - takeActionItems: getTakeActionItems, - }), - [alertsGroupsData?.aggregations, getGrouping, getTakeActionItems, isLoadingGroups, loading] + const resetGroupChildrenPagination = (parentLevel: number) => { + setPageIndex((allPages) => { + const resetPages = allPages.splice(parentLevel + 1, allPages.length); + return [...allPages, ...resetPages.map(() => DEFAULT_PAGE_INDEX)]; + }); + }; + return ( + resetGroupChildrenPagination(level)} + pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX} + pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE} + parentGroupingFilter={parentGroupingFilter} + renderChildComponent={rcc} + selectedGroup={selectedGroup} + setPageIndex={(newIndex: number) => setPageVar(newIndex, level, 'index')} + setPageSize={(newSize: number) => setPageVar(newSize, level, 'size')} + /> + ); + }, + [getGrouping, pageIndex, pageSize, props, selectedGroups, setPageVar] ); if (isEmpty(selectedPatterns)) { return null; } - return groupedAlerts; + return getLevel(0, selectedGroups[0]); }; export const GroupedAlertsTable = React.memo(GroupedAlertsTableComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx new file mode 100644 index 0000000000000..cf8a8128cb166 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx @@ -0,0 +1,259 @@ +/* + * 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 React, { useCallback, useEffect, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; +import type { GroupingAggregation } from '@kbn/securitysolution-grouping'; +import { isNoneGroup } from '@kbn/securitysolution-grouping'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { DynamicGroupingProps } from '@kbn/securitysolution-grouping/src'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; +import { combineQueries } from '../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import type { Status } from '../../../../common/detection_engine/schemas/common'; +import { InspectButton } from '../../../common/components/inspect'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useKibana } from '../../../common/lib/kibana'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { useInspectButton } from '../alerts_kpis/common/hooks'; +import { buildTimeRangeFilter } from './helpers'; + +import * as i18n from './translations'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; +import { getAlertsGroupingQuery, useGroupTakeActionsItems } from './grouping_settings'; + +const ALERTS_GROUPING_ID = 'alerts-grouping'; + +interface OwnProps { + currentAlertStatusFilterValue?: Status[]; + defaultFilters?: Filter[]; + from: string; + getGrouping: ( + props: Omit, 'groupSelector' | 'pagination'> + ) => React.ReactElement; + globalFilters: Filter[]; + globalQuery: Query; + groupingLevel?: number; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; + loading: boolean; + onGroupClose: () => void; + pageIndex: number; + pageSize: number; + parentGroupingFilter?: string; + renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement; + runtimeMappings: MappingRuntimeFields; + selectedGroup: string; + setPageIndex: (newIndex: number) => void; + setPageSize: (newSize: number) => void; + signalIndexName: string | null; + tableId: TableIdLiteral; + to: string; +} + +export type AlertsTableComponentProps = OwnProps; + +export const GroupedSubLevelComponent: React.FC = ({ + currentAlertStatusFilterValue, + defaultFilters = [], + from, + getGrouping, + globalFilters, + globalQuery, + groupingLevel, + hasIndexMaintenance, + hasIndexWrite, + loading, + onGroupClose, + pageIndex, + pageSize, + parentGroupingFilter, + renderChildComponent, + runtimeMappings, + selectedGroup, + setPageIndex, + setPageSize, + signalIndexName, + tableId, + to, +}) => { + const { + services: { uiSettings }, + } = useKibana(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.detections); + + const getGlobalQuery = useCallback( + (customFilters: Filter[]) => { + if (browserFields != null && indexPattern != null) { + return combineQueries({ + config: getEsQueryConfig(uiSettings), + dataProviders: [], + indexPattern, + browserFields, + filters: [ + ...(defaultFilters ?? []), + ...globalFilters, + ...customFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ...buildTimeRangeFilter(from, to), + ], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + }); + } + return null; + }, + [ + browserFields, + defaultFilters, + from, + globalFilters, + globalQuery, + indexPattern, + parentGroupingFilter, + to, + uiSettings, + ] + ); + + const additionalFilters = useMemo(() => { + try { + return [ + buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ + ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), + ...(defaultFilters ?? []), + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]), + ]; + } catch (e) { + return []; + } + }, [defaultFilters, globalFilters, globalQuery, parentGroupingFilter]); + + const queryGroups = useMemo(() => { + return getAlertsGroupingQuery({ + additionalFilters, + selectedGroup, + from, + runtimeMappings, + to, + pageSize, + pageIndex, + }); + }, [additionalFilters, from, pageIndex, pageSize, runtimeMappings, selectedGroup, to]); + + const emptyGlobalQuery = useMemo(() => getGlobalQuery([]), [getGlobalQuery]); + + useInvalidFilterQuery({ + id: tableId, + filterQuery: emptyGlobalQuery?.filterQuery, + kqlError: emptyGlobalQuery?.kqlError, + query: globalQuery, + startDate: from, + endDate: to, + }); + + const { + data: alertsGroupsData, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery: setAlertsQuery, + } = useQueryAlerts<{}, GroupingAggregation>({ + query: queryGroups, + indexName: signalIndexName, + queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, + skip: isNoneGroup([selectedGroup]), + }); + + useEffect(() => { + if (!isNoneGroup([selectedGroup])) { + setAlertsQuery(queryGroups); + } + }, [queryGroups, selectedGroup, setAlertsQuery]); + + const { deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); + + useInspectButton({ + deleteQuery, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery, + uniqueQueryId, + }); + + const inspect = useMemo( + () => ( + + ), + [uniqueQueryId] + ); + + const takeActionItems = useGroupTakeActionsItems({ + indexName: indexPattern.title, + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + }); + + const getTakeActionItems = useCallback( + (groupFilters: Filter[], groupNumber: number) => + takeActionItems({ + groupNumber, + query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, + selectedGroup, + tableId, + }), + [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] + ); + + return useMemo( + () => + getGrouping({ + activePage: pageIndex, + data: alertsGroupsData?.aggregations, + groupingLevel, + inspectButton: inspect, + isLoading: loading || isLoadingGroups, + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage: (size: number) => setPageSize(size), + onChangeGroupsPage: (index) => setPageIndex(index), + renderChildComponent, + onGroupClose, + selectedGroup, + takeActionItems: getTakeActionItems, + }), + [ + alertsGroupsData?.aggregations, + getGrouping, + getTakeActionItems, + groupingLevel, + inspect, + isLoadingGroups, + loading, + pageIndex, + pageSize, + renderChildComponent, + onGroupClose, + selectedGroup, + setPageIndex, + setPageSize, + ] + ); +}; + +export const GroupedSubLevel = React.memo(GroupedSubLevelComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx index f84305dcb3b37..d663b2abc2c61 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx @@ -30,7 +30,7 @@ describe('useGroupTakeActionsItems', () => { groupNumber: 0, selectedGroup: 'test', }; - it('returns array take actions items available for alerts table if showAlertStatusActions is true', async () => { + it('returns all take actions items if showAlertStatusActions is true and currentStatus is undefined', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -47,7 +47,106 @@ describe('useGroupTakeActionsItems', () => { }); }); - it('returns empty array of take actions items available for alerts table if showAlertStatusActions is false', async () => { + it('returns all take actions items if currentStatus is []', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: [], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns all take actions items if currentStatus.length > 1', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open', 'closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns acknowledged & closed take actions items if currentStatus === ["open"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('acknowledge'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns open & acknowledged take actions items if currentStatus === ["closed"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('acknowledge'); + }); + }); + + it('returns open & closed take actions items if currentStatus === ["acknowledged"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['acknowledged'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns empty take actions items if showAlertStatusActions is false', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -63,4 +162,20 @@ describe('useGroupTakeActionsItems', () => { expect(result.current(getActionItemsParams).length).toEqual(0); }); }); + it('returns array take actions items if showAlertStatusActions is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx index d2baadb99d124..5d151d2e4cc88 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Status } from '../../../../../common/detection_engine/schemas/common'; @@ -30,8 +30,9 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from '../translations'; import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; import type { StartServices } from '../../../../types'; + export interface TakeActionsProps { - currentStatus?: Status; + currentStatus?: Status[]; indexName: string; showAlertStatusActions?: boolean; } @@ -182,7 +183,7 @@ export const useGroupTakeActionsItems = ({ ] ); - const items = useMemo(() => { + return useMemo(() => { const getActionItems = ({ query, tableId, @@ -196,61 +197,89 @@ export const useGroupTakeActionsItems = ({ }) => { const actionItems: JSX.Element[] = []; if (showAlertStatusActions) { - if (currentStatus !== FILTER_OPEN) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_OPEN as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_OPEN_SELECTED} - - ); - } - if (currentStatus !== FILTER_ACKNOWLEDGED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_ACKNOWLEDGED_SELECTED} - - ); - } - if (currentStatus !== FILTER_CLOSED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_CLOSED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_CLOSE_SELECTED} - + if (currentStatus && currentStatus.length === 1) { + const singleStatus = currentStatus[0]; + if (singleStatus !== FILTER_OPEN) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_OPEN as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_OPEN_SELECTED} + + ); + } + if (singleStatus !== FILTER_ACKNOWLEDGED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_ACKNOWLEDGED_SELECTED} + + ); + } + if (singleStatus !== FILTER_CLOSED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_CLOSED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_CLOSE_SELECTED} + + ); + } + } else { + const statusArr = { + [FILTER_OPEN]: BULK_ACTION_OPEN_SELECTED, + [FILTER_ACKNOWLEDGED]: BULK_ACTION_ACKNOWLEDGED_SELECTED, + [FILTER_CLOSED]: BULK_ACTION_CLOSE_SELECTED, + }; + Object.keys(statusArr).forEach((workflowStatus) => + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: workflowStatus as AlertWorkflowStatus, + tableId, + }) + } + > + {statusArr[workflowStatus]} + + ) ); } } @@ -259,6 +288,4 @@ export const useGroupTakeActionsItems = ({ return getActionItems; }, [currentStatus, onClickUpdate, showAlertStatusActions]); - - return items; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts new file mode 100644 index 0000000000000..9e0b4e63715aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts @@ -0,0 +1,1736 @@ +/* + * 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 { mockAlertSearchResponse } from '../../../../common/components/alerts_treemap/lib/mocks/mock_alert_search_response'; + +export const groupingSearchResponse = { + ruleName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 6048, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 32, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: ['critical hosts [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical hosts', 'f'], + key_as_string: 'critical hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts', 'f'], + key_as_string: 'high hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts ', 'f'], + key_as_string: 'low hosts |f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts', 'f'], + key_as_string: 'medium hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 91, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 91, + }, + { + key: 'rule', + doc_count: 91, + }, + ], + }, + unitsCount: { + value: 91, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 91, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + ], + }, + unitsCount: { + value: 6048, + }, + }, + }, + hostName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 900, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 40, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Host-f0m6ngo8fo', + doc_count: 75, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 75, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 75, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 25, + }, + }, + { + key: 'Host-4aijlqggv8', + doc_count: 63, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 63, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 63, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 21, + }, + }, + { + key: 'Host-e50lhbdm91', + doc_count: 51, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 51, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 51, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 17, + }, + }, + { + key: 'sqp', + doc_count: 42, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 42, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 42, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'sUl', + doc_count: 33, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 33, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 33, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'vLJ', + doc_count: 30, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 30, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 30, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-n28uwmsqmd', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 9, + }, + }, + { + key: 'JaE', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'CUA', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'FWT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'ZqT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'mmn', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xRS', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'HiC', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-d7zbfvl3zz', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 7, + }, + }, + { + key: 'Nnc', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'OqH', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Vaw', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'XPg', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qBS', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'rwt', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xVJ', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Bxg', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'efP', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qcb', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + ], + }, + unitsCount: { + value: 900, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts index 624b343c14cf9..921a8d3e3d43f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts @@ -42,8 +42,8 @@ export const getAlertsGroupingQuery = ({ getGroupingQuery({ additionalFilters, from, - groupByFields: !isNoneGroup(selectedGroup) ? getGroupFields(selectedGroup) : [], - statsAggregations: !isNoneGroup(selectedGroup) + groupByFields: !isNoneGroup([selectedGroup]) ? getGroupFields(selectedGroup) : [], + statsAggregations: !isNoneGroup([selectedGroup]) ? getAggregationsByGroupField(selectedGroup) : [], pageNumber: pageIndex * pageSize, @@ -51,7 +51,7 @@ export const getAlertsGroupingQuery = ({ { unitsCount: { value_count: { field: selectedGroup } }, }, - ...(!isNoneGroup(selectedGroup) + ...(!isNoneGroup([selectedGroup]) ? [{ groupsCount: { cardinality: { field: selectedGroup } } }] : []), ], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx deleted file mode 100644 index 346e4b51df72d..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -/* - * 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 React from 'react'; -import { render } from '@testing-library/react'; -import type { Filter } from '@kbn/es-query'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import '../../../common/mock/match_media'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../common/mock'; -import type { AlertsTableComponentProps } from './alerts_grouping'; -import { GroupedAlertsTableComponent } from './alerts_grouping'; -import { TableId } from '@kbn/securitysolution-data-table'; -import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; -import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; -import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; - -jest.mock('@kbn/securitysolution-grouping'); - -jest.mock('../../../common/containers/sourcerer'); -jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest.fn().mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: jest.fn(), - }), -})); - -jest.mock('./grouping_settings', () => ({ - getAlertsGroupingQuery: jest.fn(), - getDefaultGroupingOptions: () => [ - { label: 'ruleName', key: 'kibana.alert.rule.name' }, - { label: 'userName', key: 'user.name' }, - { label: 'hostName', key: 'host.name' }, - { label: 'sourceIP', key: 'source.ip' }, - ], - getSelectedGroupBadgeMetrics: jest.fn(), - getSelectedGroupButtonContent: jest.fn(), - getSelectedGroupCustomMetrics: jest.fn(), - useGroupTakeActionsItems: jest.fn(), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -jest.mock('../../../common/utils/normalize_time_range'); - -const mockUseFieldBrowserOptions = jest.fn(); -jest.mock('../../../timelines/components/fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockFilterManager = createFilterManagerMock(); - -const mockKibanaServices = createStartServicesMock(); - -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - ...mockKibanaServices, - application: { - navigateToUrl: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: { - ui: { getCasesContext: mockCasesContext }, - }, - uiSettings: { - get: jest.fn(), - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: mockFilterManager, - }, - }, - docLinks: { - links: { - siem: { - privileges: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - triggerActionsUi: { - getAlertsStateTable: jest.fn(() => <>), - alertsTableConfigurationRegistry: {}, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - }; -}); -const state: State = { - ...mockGlobalState, -}; -const { storage } = createSecuritySolutionStorageMock(); -const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - -const groupingStore = createStore( - { - ...state, - groups: { - groupSelector: <>, - selectedGroup: 'host.name', - }, - }, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage -); - -jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ - useAddBulkToTimelineAction: jest.fn(() => {}), -})); - -const sourcererDataView = { - indicesExist: true, - loading: false, - indexPattern: { - fields: [], - }, - browserFields: {}, -}; -const renderChildComponent = (groupingFilters: Filter[]) =>

; - -const testProps: AlertsTableComponentProps = { - defaultFilters: [], - from: '2020-07-07T08:20:18.966Z', - globalFilters: [], - globalQuery: { - query: 'query', - language: 'language', - }, - hasIndexMaintenance: true, - hasIndexWrite: true, - loading: false, - renderChildComponent, - runtimeMappings: {}, - signalIndexName: 'test', - tableId: TableId.test, - to: '2020-07-08T08:20:18.966Z', -}; - -const resetPagination = jest.fn(); - -describe('GroupedAlertsTable', () => { - const getGrouping = jest.fn().mockReturnValue(); - beforeEach(() => { - jest.clearAllMocks(); - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - selectedPatterns: ['myFakebeat-*'], - }); - (isNoneGroup as jest.Mock).mockReturnValue(true); - (useGrouping as jest.Mock).mockReturnValue({ - groupSelector: <>, - getGrouping, - selectedGroup: 'host.name', - pagination: { pageSize: 1, pageIndex: 0, reset: resetPagination }, - }); - }); - - it('calls the proper initial dispatch actions for groups', () => { - render( - - - - ); - expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' - ); - expect(mockDispatch.mock.calls[1][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_SELECTED_GROUP' - ); - }); - - it('renders grouping table', async () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - - const { getByTestId } = render( - - - - ); - expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(false); - }); - - it('renders loading when expected', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - render( - - - - ); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(true); - }); - - it('resets grouping pagination when global query updates', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - const { rerender } = render( - - - - ); - // called on initial query definition - expect(resetPagination).toHaveBeenCalledTimes(1); - rerender( - - - - ); - expect(resetPagination).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index 78d736e99c93e..92f77c3e2df91 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { isNoneGroup } from '@kbn/securitysolution-grouping'; import { dataTableSelectors, tableDefaults, @@ -29,9 +28,6 @@ export const getPersistentControlsHook = (tableId: TableId) => { const getGroupSelector = groupSelectors.getGroupSelector(); const groupSelector = useSelector((state: State) => getGroupSelector(state)); - const getSelectedGroup = groupSelectors.getSelectedGroup(); - - const selectedGroup = useSelector((state: State) => getSelectedGroup(state)); const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); @@ -88,10 +84,10 @@ export const getPersistentControlsHook = (tableId: TableId) => { hasRightOffset={false} additionalFilters={additionalFiltersComponent} showInspect={false} - additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupSelector] : []} + additionalMenuOptions={groupSelector != null ? [groupSelector] : []} /> ), - [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector, selectedGroup] + [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector] ); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 64e489599dd05..13836e658eee7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; +import { render, waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { createSecuritySolutionStorageMock, @@ -29,6 +28,10 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; +import type { FilterGroupProps } from '../../../common/components/filter_group/types'; +import { FilterGroup } from '../../../common/components/filter_group'; +import type { AlertsTableComponentProps } from '../../components/alerts_table/alerts_grouping'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -38,6 +41,27 @@ jest.mock('../../../common/components/search_bar', () => ({ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../../common/hooks/use_space_id', () => ({ + useSpaceId: () => 'default', +})); +jest.mock('../../../common/components/filter_group'); + +const mockStatusCapture = jest.fn(); +const GroupedAlertsTable: React.FC = ({ + currentAlertStatusFilterValue, +}) => { + useEffect(() => { + if (currentAlertStatusFilterValue) { + mockStatusCapture(currentAlertStatusFilterValue); + } + }, [currentAlertStatusFilterValue]); + return ; +}; + +jest.mock('../../components/alerts_table/alerts_grouping', () => ({ + GroupedAlertsTable, +})); + jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../common/containers/sourcerer'); @@ -158,9 +182,11 @@ jest.mock('../../../common/components/page/use_refetch_by_session'); describe('DetectionEnginePageComponent', () => { beforeAll(() => { + (useListsConfig as jest.Mock).mockReturnValue({ loading: false, needsConfiguration: false }); (useParams as jest.Mock).mockReturnValue({}); (useUserData as jest.Mock).mockReturnValue([ { + loading: false, hasIndexRead: true, canUserREAD: true, }, @@ -170,10 +196,15 @@ describe('DetectionEnginePageComponent', () => { indexPattern: {}, browserFields: mockBrowserFields, }); + (FilterGroup as jest.Mock).mockImplementation(() => { + return ; + }); + }); + beforeEach(() => { + jest.clearAllMocks(); }); - it('renders correctly', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -181,12 +212,12 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + expect(getByTestId('filter-group__loading')).toBeInTheDocument(); }); }); it('renders the chart panels', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -195,7 +226,119 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="chartPanels"]').exists()).toBe(true); + expect(getByTestId('chartPanels')).toBeInTheDocument(); + }); + }); + + it('the pageFiltersUpdateHandler updates status when a multi status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + params: ['open', 'acknowledged'], + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open', 'acknowledged']); + }); + + it('the pageFiltersUpdateHandler updates status when a single status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open']); + }); + + it('the pageFiltersUpdateHandler clears status when no status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, []); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 0eacecd46e8fc..1ccfaea4584ee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -29,7 +29,6 @@ import { dataTableActions, dataTableSelectors, tableDefaults, - FILTER_OPEN, TableId, } from '@kbn/securitysolution-data-table'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants'; @@ -139,7 +138,7 @@ const DetectionEnginePageComponent: React.FC = ({ const arePageFiltersEnabled = useIsExperimentalFeatureEnabled('alertsPageFiltersEnabled'); // when arePageFiltersEnabled === false - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [statusFilter, setStatusFilter] = useState([]); const updatedAt = useShallowEqualSelector( (state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).updated @@ -177,8 +176,8 @@ const DetectionEnginePageComponent: React.FC = ({ if (arePageFiltersEnabled) { return detectionPageFilters; } - return buildAlertStatusFilter(filterGroup); - }, [filterGroup, detectionPageFilters, arePageFiltersEnabled]); + return buildAlertStatusFilter(statusFilter[0] ?? 'open'); + }, [statusFilter, detectionPageFilters, arePageFiltersEnabled]); useEffect(() => { if (!detectionPageFilterHandler) return; @@ -276,6 +275,19 @@ const DetectionEnginePageComponent: React.FC = ({ const pageFiltersUpdateHandler = useCallback((newFilters: Filter[]) => { setDetectionPageFilters(newFilters); + if (newFilters.length) { + const newStatusFilter = newFilters.find( + (filter) => filter.meta.key === 'kibana.alert.workflow_status' + ); + if (newStatusFilter) { + const status: Status[] = newStatusFilter.meta.params + ? (newStatusFilter.meta.params as Status[]) + : [newStatusFilter.query?.match_phrase['kibana.alert.workflow_status']]; + setStatusFilter(status); + } else { + setStatusFilter([]); + } + } }, []); // Callback for when open/closed filter changes @@ -284,9 +296,9 @@ const DetectionEnginePageComponent: React.FC = ({ const timelineId = TableId.alertsOnAlertsPage; clearEventsLoading({ id: timelineId }); clearEventsDeleted({ id: timelineId }); - setFilterGroup(newFilterGroup); + setStatusFilter([newFilterGroup]); }, - [clearEventsLoading, clearEventsDeleted, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, setStatusFilter] ); const areDetectionPageFiltersLoading = useMemo(() => { @@ -317,7 +329,7 @@ const DetectionEnginePageComponent: React.FC = ({ @@ -352,7 +364,7 @@ const DetectionEnginePageComponent: React.FC = ({ [ arePageFiltersEnabled, dataViewId, - filterGroup, + statusFilter, filters, onFilterGroupChangedCallback, pageFiltersUpdateHandler, @@ -462,7 +474,7 @@ const DetectionEnginePageComponent: React.FC = ({ { const [updatedAt, setUpdatedAt] = useState(Date.now()); const { toggleStatus, setToggleStatus } = useQueryToggle(TABLE_QUERY_ID); - const { deleteQuery, setQuery, from, to } = useGlobalTime(false); + const { deleteQuery, setQuery, from, to } = useGlobalTime(); const { isLoading: isSearchLoading, data, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index 28e5c696d1f52..85cc8c0043897 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -41,7 +41,7 @@ const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery'; const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery'; export const EntityAnalyticsHeader = () => { - const { from, to } = useGlobalTime(false); + const { from, to } = useGlobalTime(); const timerange = useMemo( () => ({ from, From b5c88d90ceac26e990dcde8c5e8df524cad0a2a8 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 24 Apr 2023 09:07:15 -0400 Subject: [PATCH 05/36] [Security Solution] Move policy meta updates to policy update (#155462) ## Summary Moving the new meta fields in Policy to update when the Policy update callback is called. These fields are used in telemetry. These fields are being moved from the Policy watcher to avoid triggering a policy deploy on many Agents at once on upgrade. Instead, these fields will be updated whenever the next Endpoint policy update comes. New Policies will have the telemetry fields already populated. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../endpoint/endpoint_app_context_services.ts | 3 +- .../endpoint/lib/policy/license_watch.test.ts | 26 +------ .../endpoint/lib/policy/license_watch.ts | 7 -- .../fleet_integration.test.ts | 69 ++++++++++++++++++- .../fleet_integration/fleet_integration.ts | 30 +++++++- .../security_solution/server/plugin.ts | 1 - 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 489953a2d96d2..8e1855b2cd84c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -125,7 +125,8 @@ export class EndpointAppContextService { logger, licenseService, featureUsageService, - endpointMetadataService + endpointMetadataService, + cloud ) ); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 9c57b8156e3e3..9d962bc0e64ce 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -16,7 +16,6 @@ import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; import { PolicyWatcher } from './license_watch'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock'; -import { cloudMock } from '@kbn/cloud-plugin/server/mocks'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; @@ -36,7 +35,6 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); - const cloudServiceMock = cloudMock.createSetup(); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; @@ -53,13 +51,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -104,13 +96,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -137,13 +123,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index dded85b559c6d..195c8509e60d5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -17,7 +17,6 @@ import type { } from '@kbn/core/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; @@ -35,19 +34,16 @@ export class PolicyWatcher { private policyService: PackagePolicyClient; private subscription: Subscription | undefined; private soStart: SavedObjectsServiceStart; - private cloud: CloudSetup; constructor( policyService: PackagePolicyClient, soStart: SavedObjectsServiceStart, esStart: ElasticsearchServiceStart, - cloud: CloudSetup, logger: Logger ) { this.policyService = policyService; this.esClient = esStart.client.asInternalUser; this.logger = logger; this.soStart = soStart; - this.cloud = cloud; } /** @@ -105,9 +101,6 @@ export class PolicyWatcher { for (const policy of response.items as PolicyData[]) { const updatePolicy = getPolicyDataForUpdate(policy); const policyConfig = updatePolicy.inputs[0].config.policy.value; - updatePolicy.inputs[0].config.policy.value.meta.license = license.type || ''; - // add cloud info to policy meta - updatePolicy.inputs[0].config.policy.value.meta.cloud = this.cloud?.isCloudEnabled; try { if (!isEndpointPolicyValidForLicense(policyConfig, license)) { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 30c7e776e718e..c415fc287fbec 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -366,7 +366,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -382,7 +383,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -412,7 +414,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -427,6 +430,66 @@ describe('ingest_integration tests ', () => { }); }); + describe('package policy update callback when meta fields should be updated', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); + it('updates successfully when meta fields differ from services', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = false; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'gold'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + + it('meta fields stay the same where there is no difference', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = true; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'platinum'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + }); + describe('package policy delete callback', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index fd6f85af1a045..7e68c63f07593 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -44,6 +44,17 @@ const isEndpointPackagePolicy = ( return packagePolicy.package?.name === 'endpoint'; }; +const shouldUpdateMetaValues = ( + endpointPackagePolicy: PolicyConfig, + currentLicenseType: string, + currentCloudInfo: boolean +) => { + return ( + endpointPackagePolicy.meta.license !== currentLicenseType || + endpointPackagePolicy.meta.cloud !== currentCloudInfo + ); +}; + /** * Callback to handle creation of PackagePolicies in Fleet */ @@ -152,7 +163,8 @@ export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService, featureUsageService: FeatureUsageService, - endpointMetadataService: EndpointMetadataService + endpointMetadataService: EndpointMetadataService, + cloud: CloudSetup ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { @@ -170,6 +182,22 @@ export const getPackagePolicyUpdateCallback = ( notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + ?.value as PolicyConfig; + + if ( + newPackagePolicy.inputs[0].config?.policy?.value && + shouldUpdateMetaValues( + newEndpointPackagePolicy, + licenseService.getLicenseType(), + cloud?.isCloudEnabled + ) + ) { + newEndpointPackagePolicy.meta.license = licenseService.getLicenseType(); + newEndpointPackagePolicy.meta.cloud = cloud?.isCloudEnabled; + newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + return newPackagePolicy; }; }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ab5ac00454a5e..e6ad26f1f405f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -472,7 +472,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.fleet.packagePolicyService, core.savedObjects, core.elasticsearch, - plugins.cloud, logger ); this.policyWatcher.start(licenseService); From b71f7831d80785df4d07831388d5d082d31ec8d6 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Mon, 24 Apr 2023 15:22:56 +0200 Subject: [PATCH 06/36] [AO] Sync chart pointers on the metric threshold alert details page (#155402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #155354 ## Summary This PR syncs chart pointers on the metric threshold alert details page. ![image](https://user-images.githubusercontent.com/12370520/233380782-bf97eab8-167d-4f97-a10d-d2bfec1936e7.png) ## 🧪 How to test - Add `xpack.observability.unsafe.alertDetails.metrics.enabled: true` to the Kibana config - Create a metric threshold rule with multiple conditions that generates an alert - Go to the alert details page and check the chart pointers --- .../components/expression_chart.test.tsx | 3 +++ .../components/expression_chart.tsx | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index f2bb22485fe9c..745a1f0169788 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -22,6 +22,9 @@ jest.mock('../../../hooks/use_kibana', () => ({ useKibanaContextForPlugin: () => ({ services: { ...mockStartServices, + charts: { + activeCursor: jest.fn(), + }, }, }), })); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 8dd7762feb6b9..8b453579b5e67 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useRef } from 'react'; import { Axis, Chart, @@ -17,6 +17,7 @@ import { } from '@elastic/charts'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { DataViewBase } from '@kbn/es-query'; import { first, last } from 'lodash'; @@ -66,7 +67,7 @@ export const ExpressionChart: React.FC = ({ timeRange, annotations, }) => { - const { uiSettings } = useKibanaContextForPlugin().services; + const { uiSettings, charts } = useKibanaContextForPlugin().services; const { isLoading, data } = useMetricsExplorerChartData( expression, @@ -77,6 +78,11 @@ export const ExpressionChart: React.FC = ({ timeRange ); + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } @@ -141,7 +147,7 @@ export const ExpressionChart: React.FC = ({ return ( <> - + = ({ tickFormat={createFormatterForMetric(metric)} domain={domain} /> - +

From 4dc21e5589067fe68ca2767204759e8579914c16 Mon Sep 17 00:00:00 2001 From: Apoorva Joshi <30438249+ajosh0504@users.noreply.github.com> Date: Mon, 24 Apr 2023 06:51:06 -0700 Subject: [PATCH 07/36] Updates to pre-built Security ML jobs (#154596) ## Summary This PR makes the following updates to the pre-built Security ML jobs: - Making the `security-packetbeat` compatible with Agent - Removing superfluous fields from the job configurations to make them consistent - Updating the `detector_description` field for almost all jobs - Adding influencers where missing and/or relevant - Adding a `job_revision` custom setting similar to the Logs [jobs](https://github.com/elastic/kibana/blob/main/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/log_entry_rate.json#L29). Moving forward, this number will be updated each time a job is updated. We are starting with 4 since the `linux` and `windows` jobs are at v3 right now - Adding a `managed`: `true` tag to indicate that these jobs are pre-configured by Elastic and so users will see the warnings added in [this](https://github.com/elastic/kibana/pull/122305) PR if users choose to delete, or modify these jobs --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../modules/security_auth/manifest.json | 4 +- .../ml/auth_high_count_logon_events.json | 14 ++--- ...gh_count_logon_events_for_a_source_ip.json | 18 ++---- .../ml/auth_high_count_logon_fails.json | 14 ++--- .../ml/auth_rare_hour_for_a_user.json | 18 +++--- .../ml/auth_rare_source_ip_for_a_user.json | 18 +++--- .../security_auth/ml/auth_rare_user.json | 18 +++--- .../datafeed_suspicious_login_activity.json | 9 +-- .../ml/suspicious_login_activity.json | 27 +++------ .../modules/security_cloudtrail/manifest.json | 12 ++-- .../ml/high_distinct_count_error_message.json | 21 +++---- .../ml/rare_error_code.json | 20 +++---- .../ml/rare_method_for_a_city.json | 20 +++---- .../ml/rare_method_for_a_country.json | 20 +++---- .../ml/rare_method_for_a_username.json | 17 +++--- .../modules/security_linux/manifest.json | 7 +-- .../v3_linux_anomalous_network_activity.json | 49 +++-------------- ...linux_anomalous_network_port_activity.json | 49 +++-------------- .../v3_linux_anomalous_process_all_hosts.json | 49 +++-------------- .../ml/v3_linux_anomalous_user_name.json | 48 +++------------- ...linux_network_configuration_discovery.json | 51 +++-------------- ...v3_linux_network_connection_discovery.json | 51 +++-------------- .../ml/v3_linux_rare_metadata_process.json | 30 +++------- .../ml/v3_linux_rare_metadata_user.json | 29 +++------- .../ml/v3_linux_rare_sudo_user.json | 51 +++-------------- .../ml/v3_linux_rare_user_compiler.json | 43 +++------------ ...v3_linux_system_information_discovery.json | 51 +++-------------- .../ml/v3_linux_system_process_discovery.json | 51 +++-------------- .../ml/v3_linux_system_user_discovery.json | 49 +++-------------- .../ml/v3_rare_process_by_host_linux.json | 48 +++------------- .../modules/security_network/manifest.json | 4 +- .../ml/high_count_by_destination_country.json | 14 ++--- .../ml/high_count_network_denies.json | 14 ++--- .../ml/high_count_network_events.json | 14 ++--- .../ml/rare_destination_country.json | 11 ++-- .../modules/security_packetbeat/manifest.json | 10 ++-- .../ml/datafeed_packetbeat_dns_tunneling.json | 16 +++--- ...datafeed_packetbeat_rare_dns_question.json | 16 +++--- .../datafeed_packetbeat_rare_user_agent.json | 16 +++--- .../ml/packetbeat_dns_tunneling.json | 29 +++------- .../ml/packetbeat_rare_dns_question.json | 22 ++------ .../ml/packetbeat_rare_server_domain.json | 24 ++------ .../ml/packetbeat_rare_urls.json | 23 ++------ .../ml/packetbeat_rare_user_agent.json | 23 ++------ .../ml/v3_rare_process_by_host_windows.json | 53 +++--------------- ...v3_windows_anomalous_network_activity.json | 53 +++--------------- .../v3_windows_anomalous_path_activity.json | 52 +++--------------- ...3_windows_anomalous_process_all_hosts.json | 55 +++---------------- ...v3_windows_anomalous_process_creation.json | 53 +++--------------- .../ml/v3_windows_anomalous_script.json | 42 +++----------- .../ml/v3_windows_anomalous_service.json | 37 +++---------- .../ml/v3_windows_anomalous_user_name.json | 53 +++--------------- .../ml/v3_windows_rare_metadata_process.json | 34 +++--------- .../ml/v3_windows_rare_metadata_user.json | 33 +++-------- .../ml/v3_windows_rare_user_runas_event.json | 46 ++-------------- ...windows_rare_user_type10_remote_login.json | 46 ++-------------- 56 files changed, 386 insertions(+), 1313 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json index b3395d82a9c29..d600e4a637acf 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json @@ -2,7 +2,7 @@ "id": "security_auth", "title": "Security: Authentication", "description": "Detect anomalous activity in your ECS-compatible authentication logs.", - "type": "auth data", + "type": "Auth data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index 7ca7a5ebd71e4..ac50e2f53535c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon events", + "detector_description": "Detects high count of logon events.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events" + "security_app_display_name": "Spike in Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 47096f4c6413f..d23f8df88ef6a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -1,25 +1,17 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of auth events for a source IP", + "detector_description": "Detects high count of auth events for a source IP.", "function": "high_non_zero_count", "by_field_name": "source.ip", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "winlog.event_data.LogonType", - "user.name" - ], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events from a Source IP" + "security_app_display_name": "Spike in Logon Events from a Source IP", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 48586ef642ca6..db2db5ea00832 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon fails", + "detector_description": "Detects high count of logon fails.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Failed Logon Events" + "security_app_display_name": "Spike in Failed Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json index 1f421ed298b9f..57477497aeb62 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare hour for a user", + "detector_description": "Detects rare hour for a user.", "function": "time_of_day", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Hour for a User to Logon" + "security_app_display_name": "Unusual Hour for a User to Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json index 98a249074a67a..81185ef5039c7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json @@ -1,24 +1,18 @@ { - "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare source IP for a user", + "detector_description": "Detects rare source IP for a user.", "function": "rare", "by_field_name": "source.ip", "partition_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Source IP for a User to Logon from" + "security_app_display_name": "Unusual Source IP for a User to Logon from", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json index e2488480e61d1..58530fe085014 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare user", + "detector_description": "Detects rare user authentication.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Rare User Logon" + "security_app_display_name": "Rare User Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json index 386b9fab25667..59a9129e7b7bf 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json @@ -1,15 +1,10 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": { "event.category": "authentication" }}, - {"term": { "agent.type": "auditbeat" }} - ] + "filter": [{ "term": { "event.category": "authentication" } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json index 00e810b5348e7..bbe420b3ec0eb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json @@ -1,24 +1,17 @@ { - "description": "Security: Auditbeat - Detect unusually high number of authentication attempts.", - "groups": [ - "security", - "auditbeat", - "authentication" - ], + "description": "Security: Authentication - Detects unusually high number of authentication attempts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high number of authentication attempts", + "detector_description": "Detects high number of authentication attempts for a host.", "function": "high_non_zero_count", - "partition_field_name": "host.name" + "partition_field_name": "host.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "source.ip" - ], + "influencers": ["host.name", "user.name", "source.ip"], "model_prune_window": "30d" }, "allow_lazy_open": true, @@ -31,11 +24,7 @@ "custom_settings": { "created_by": "ml-module-security-auth", "security_app_display_name": "Unusual Login Activity", - "custom_urls": [ - { - "url_name": "IP Address Details", - "url_value": "security/network/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json index 93797b9e3e758..52b406a0da7cb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json @@ -1,16 +1,14 @@ { "id": "security_cloudtrail", "title": "Security: Cloudtrail", - "description": "Detect suspicious activity recorded in your cloudtrail logs.", - "type": "Filebeat data", + "description": "Detect suspicious activity recorded in Cloudtrail logs.", + "type": "Cloudtrail data", "logoFile": "logo.json", - "defaultIndexPattern": "filebeat-*", + "defaultIndexPattern": "logs-*,filebeat-*", "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "aws.cloudtrail"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "event.dataset": "aws.cloudtrail" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json index 11b5f4625a484..2ba7c4fdf4085 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json @@ -1,24 +1,17 @@ { "description": "Security: Cloudtrail - Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", + "detector_description": "Detects high distinct count of Cloudtrail error messages.", "function": "high_distinct_count", - "field_name": "aws.cloudtrail.error_message" + "field_name": "aws.cloudtrail.error_message", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ], - "model_prune_window": "30d" + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Spike in AWS Error Messages" + "security_app_display_name": "Spike in AWS Error Messages", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json index c54c8e8378f2c..7752430876e3f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json @@ -1,23 +1,17 @@ { "description": "Security: Cloudtrail - Looks for unusual errors. Rare and unusual errors may simply indicate an impending service failure but they can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"aws.cloudtrail.error_code\"", + "detector_description": "Detects rare Cloudtrail error codes.", "function": "rare", - "by_field_name": "aws.cloudtrail.error_code" + "by_field_name": "aws.cloudtrail.error_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Rare AWS Error Code" + "security_app_display_name": "Rare AWS Error Code", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json index 2ed28884be94f..f7be6fe8cc8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (city) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", + "detector_description": "Detects rare event actions for a city.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.city_name" + "partition_field_name": "source.geo.city_name", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual City for an AWS Command" + "security_app_display_name": "Unusual City for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json index 1f14357e73444..d73f51f34de3a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (country) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", + "detector_description": "Detects rare event actions for an ISO code.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.country_iso_code" + "partition_field_name": "source.geo.country_iso_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.country_iso_code" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.country_iso_code"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual Country for an AWS Command" + "security_app_display_name": "Unusual Country for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json index 76cce7fb829ca..a508028619833 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json @@ -1,23 +1,22 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a user context that does not normally call the method. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"user.name\"", + "detector_description": "Detects rare event actions for a user.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "user.name" + "partition_field_name": "user.name", + "detector_index": 0 } ], "influencers": [ "user.name", "source.ip", - "source.geo.city_name" + "source.geo.city_name", + "aws.cloudtrail.user_identity.arn" ] }, "allow_lazy_open": true, @@ -29,6 +28,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual AWS Command for a User" + "security_app_display_name": "Unusual AWS Command for a User", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json index 269f90dea4471..cfff61e304c0e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json @@ -2,7 +2,7 @@ "id": "security_linux_v3", "title": "Security: Linux", "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.", - "type": "linux data", + "type": "Linux data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*", "query": { @@ -43,10 +43,7 @@ ], "must_not": { "terms": { - "_tier": [ - "data_frozen", - "data_cold" - ] + "_tier": ["data_frozen", "data_cold"] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json index 29f6bf1d98412..b276bcc7856ba 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "network", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", - "by_field_name": "process.name" + "by_field_name": "process.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Activity" + "security_app_display_name": "Unusual Linux Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json index 34b97358260ac..a551d6c2c204f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "network" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare destination.port values.", + "detector_description": "Detects rare destination ports.", "function": "rare", - "by_field_name": "destination.port" + "by_field_name": "destination.port", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Port Activity" + "security_app_display_name": "Unusual Linux Network Port Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json index a20a508391fb9..dea5fa3a5db31 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json @@ -1,65 +1,30 @@ { "description": "Security: Linux - Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "512mb", - "categorization_examples_limit": 4 - + "model_memory_limit": "512mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Linux Population" + "security_app_display_name": "Anomalous Process for a Linux Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json index 72be89bd79aad..05d46860b145f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json @@ -1,64 +1,30 @@ { "description": "Security: Linux - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Username" + "security_app_display_name": "Unusual Linux Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json index 1481b7a03a559..fccfa9493e8c2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network configuration discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network configuration discovery to increase their understanding of connected networks and hosts. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "40012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Network Configuration Discovery" + "security_app_display_name": "Unusual Linux Network Configuration Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json index 2b1cf43ac94d3..32dc04c079db1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network connection discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network connection discovery to increase their understanding of connected services and systems. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Connection Discovery" + "security_app_display_name": "Unusual Linux Network Connection Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json index fcec32acd69b5..6897876ad6ba3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json @@ -1,46 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "process.name" - ] + "influencers": ["host.name", "user.name", "process.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux Process Calling the Metadata Service" + "security_app_display_name": "Unusual Linux Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json index d8414c8bf22bd..ad81023d69383 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json @@ -1,45 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4010", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux User Calling the Metadata Service" + "security_app_display_name": "Unusual Linux User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json index a99e5f95572f7..11be6277c4220 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for sudo activity from an unusual user context. Unusual user context changes can be due to privilege escalation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4017", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Sudo Activity" + "security_app_display_name": "Unusual Sudo Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json index 9c8ca5316ace3..08dbbc60d02f7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.title", - "host.name", - "process.working_directory", - "user.name" - ] + "influencers": ["process.title", "host.name", "process.working_directory", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4018", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Linux Compiler Activity" + "security_app_display_name": "Anomalous Linux Compiler Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json index 0202854934285..255d0347654b0 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system information discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system information discovery to gather detailed information about system configuration and software versions. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4014", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Information Discovery Activity" + "security_app_display_name": "Unusual Linux System Information Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json index 23e6e607ccf08..03e57ce2237af 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system process discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system process discovery to increase their understanding of software applications running on a target host or network. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4015", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Process Discovery Activity" + "security_app_display_name": "Unusual Linux Process Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json index 8659e7a8f1f91..2b1c4dc595777 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system user or owner discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system owner or user discovery to identify currently active or primary users of a system. This may be a precursor to additional discovery, credential dumping, or privilege elevation activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4016", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Owner or User Discovery Activity" + "security_app_display_name": "Unusual Linux User Discovery Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json index a072007a0f13c..ce0e7f413f676 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json @@ -1,65 +1,31 @@ { "description": "Security: Linux - Looks for processes that are unusual to a particular Linux host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes for a host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Linux Host" + "security_app_display_name": "Unusual Process for a Linux Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json index bed522d4e954a..edf6c66a213bd 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json @@ -2,7 +2,7 @@ "id": "security_network", "title": "Security: Network", "description": "Detect anomalous network activity in your ECS-compatible network logs.", - "type": "network data", + "type": "Network data", "logoFile": "logo.json", "defaultIndexPattern": "logs-*,filebeat-*,packetbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index 4479fe8f8c662..b19a3f0e27812 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_non_zero_count by \"destination.geo.country_name\"", + "detector_description": "Detects high count by country.", "function": "high_non_zero_count", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -19,8 +16,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic to a Country" + "security_app_display_name": "Spike in Network Traffic to a Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index 984bfea22fa2d..1477e951d3ce9 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network denies.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.port" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Firewall Denies" + "security_app_display_name": "Spike in Firewall Denies", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index ba740d581a27e..81b516204fbc1 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network events.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic" + "security_app_display_name": "Spike in Network Traffic", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json index 123b802c475fb..4b8799d65b746 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - looks for an unusual destination country name in the network logs. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from a server in a country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"destination.geo.country_name\"", + "detector_description": "Detects rare country names.", "function": "rare", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -30,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Network Traffic to Rare Destination Country" + "security_app_display_name": "Network Traffic to Rare Destination Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json index f7a65d0137f26..799363b8fbac1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json @@ -1,16 +1,14 @@ { "id": "security_packetbeat", "title": "Security: Packetbeat", - "description": "Detect suspicious network activity in Packetbeat data.", + "description": "Detect suspicious activity in Packetbeat data.", "type": "Packetbeat data", "logoFile": "logo.json", - "defaultIndexPattern": "packetbeat-*", + "defaultIndexPattern": "packetbeat-*,logs-*", "query": { "bool": { - "filter": [ - {"term": {"agent.type": "packetbeat"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json index 449c8af238b56..334435732a07e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"destination.ip": "169.254.169.254"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "destination.ip": "169.254.169.254" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json index 3a4055eb55ba0..fe87d86ee352f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"dns.question.type": "PTR"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "dns.question.type": "PTR" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json index 5986c326ea80f..79a297595d8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "http"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "http" } }, + { "term": { "event.dataset": "network_traffic.http" } } ], - "must_not": [ - {"wildcard": {"user_agent.original": {"value": "Mozilla*"}}} - ] + "minimum_should_match": 1, + "must_not": [{ "wildcard": { "user_agent.original": { "value": "Mozilla*" } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json index 313bd8e1bea39..54b8ddf2e7a14 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json @@ -1,23 +1,17 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_info_content(\"dns.question.name\") over tld", + "detector_description": "Detects high info content of DNS questions over a population of TLDs.", "function": "high_info_content", "field_name": "dns.question.name", "over_field_name": "dns.question.etld_plus_one", "custom_rules": [ { - "actions": [ - "skip_result" - ], + "actions": ["skip_result"], "conditions": [ { "applies_to": "actual", @@ -29,12 +23,7 @@ ] } ], - "influencers": [ - "destination.ip", - "host.name", - "dns.question.etld_plus_one" - ], - "model_prune_window": "30d" + "influencers": ["destination.ip", "host.name", "dns.question.etld_plus_one"] }, "allow_lazy_open": true, "analysis_limits": { @@ -45,12 +34,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "DNS Tunneling" + "security_app_display_name": "DNS Tunneling", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json index 36c8b3acd722e..049d4e3babd23 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json @@ -1,22 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"dns.question.name\"", + "detector_description": "Detects rare DNS question names.", "function": "rare", "by_field_name": "dns.question.name" } ], - "influencers": [ - "host.name" - ] + "influencers": ["host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -27,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual DNS Activity" + "security_app_display_name": "Unusual DNS Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json index 3f3c137e8fd34..d8df5c4986b99 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json @@ -1,24 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP or TLS destination domain activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"server.domain\"", + "detector_description": "Detects rare server domains.", "function": "rare", "by_field_name": "server.domain" } ], - "influencers": [ - "host.name", - "destination.ip", - "source.ip" - ] + "influencers": ["host.name", "destination.ip", "source.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Network Destination Domain Name" + "security_app_display_name": "Unusual Network Destination Domain Name", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json index afa430bd835f2..055204dd1c376 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"url.full\"", + "detector_description": "Detects rare URLs.", "function": "rare", "by_field_name": "url.full" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web Request" + "security_app_display_name": "Unusual Web Request", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json index bb2d524b41c1f..c947e4f1d509b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"user_agent.original\"", + "detector_description": "Detects rare web user agents.", "function": "rare", "by_field_name": "user_agent.original" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web User Agent" + "security_app_display_name": "Unusual Web User Agent", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json index 6b7e5dcf56f1f..38fa9e2e4e904 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for processes that are unusual to a particular Windows host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes per host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8001", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Windows Host" + "security_app_display_name": "Unusual Process for a Windows Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json index 04ee9912c15e3..2e04fa91be336 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "64mb", - "categorization_examples_limit": 4 + "model_memory_limit": "64mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Network Activity" + "security_app_display_name": "Unusual Windows Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json index d5c931b3c46e8..c9f0579309c6b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json @@ -1,65 +1,29 @@ { "description": "Security: Windows - Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.working_directory values.", + "detector_description": "Detects rare working directories.", "function": "rare", "by_field_name": "process.working_directory", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Path Activity" + "security_app_display_name": "Unusual Windows Path Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json index 1474763cec7b9..08baa6587f9ff 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.executable values.", + "detector_description": "Detects rare process executable values.", "function": "rare", - "by_field_name": "process.executable", + "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Windows Population" + "security_app_display_name": "Anomalous Process for a Windows Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json index 2966630fad878..1bf46c2d416a9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each process.parent.name, detects rare process.name values.", + "detector_description": "Detects rare processes per parent process.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "process.parent.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Windows Process Creation" + "security_app_display_name": "Anomalous Windows Process Creation", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json index b01641b2ef3ad..5472ad77e1b70 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json @@ -1,28 +1,17 @@ { "description": "Security: Windows - Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "windows", - "winlogbeat", - "powershell", - "security" - ], + "groups": ["windows", "powershell", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects high information content in powershell.file.script_block_text values.", + "detector_description": "Detects high information content in powershell scripts.", "function": "high_info_content", - "field_name": "powershell.file.script_block_text" + "field_name": "powershell.file.script_block_text", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "file.path" - ] + "influencers": ["host.name", "user.name", "file.path"] }, "allow_lazy_open": true, "analysis_limits": { @@ -32,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8006", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Suspicious Powershell Script" + "security_app_display_name": "Suspicious Powershell Script", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json index 9716c8365e317..b2530538a9263 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json @@ -1,27 +1,17 @@ { - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "description": "Security: Windows - Looks for rare and unusual Windows service names which may indicate execution of unauthorized services, malware, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare winlog.event_data.ServiceName values.", + "detector_description": "Detects rare service names.", "function": "rare", - "by_field_name": "winlog.event_data.ServiceName" + "by_field_name": "winlog.event_data.ServiceName", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "winlog.event_data.ServiceName" - ] + "influencers": ["host.name", "winlog.event_data.ServiceName"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,20 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8007", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Service" + "security_app_display_name": "Unusual Windows Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json index eda4b768b5308..659e58cfdba32 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Username" + "security_app_display_name": "Unusual Windows Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json index ab4fd311d6646..953a00a8fff52 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json @@ -1,47 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "security", - "endpoint", - "process", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare process names.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "user.name" - ] + "influencers": ["process.name", "host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8011", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows Process Calling the Metadata Service" + "security_app_display_name": "Unusual Windows Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json index fe8a634d49921..df55cb3d67709 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json @@ -1,46 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "endpoint", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows User Calling the Metadata Service" + "security_app_display_name": "Unusual Windows User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json index b95aa1144f440..87d9d4b172f63 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual user context switches can be due to privilege escalation.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows User Privilege Elevation Activity" + "security_app_display_name": "Unusual Windows User Privilege Elevation Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json index a6ec19401190f..e118f761453be 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Remote User" + "security_app_display_name": "Unusual Windows Remote User", + "managed": true, + "job_revision": 4 } } From 069324a823b2f7e2306c68ab81f913e9fd80472b Mon Sep 17 00:00:00 2001 From: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:59:36 +0200 Subject: [PATCH 08/36] [Enterprise Search] Use caching for filtered config fields (#155608) Moves the configurable fields filtering to the logic file so it can make use of caching. --- .../connector_configuration_form.tsx | 28 +- .../connector_configuration_logic.test.ts | 348 +++++++++++++++++- .../connector_configuration_logic.ts | 29 +- 3 files changed, 376 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx index 7627c5c869469..2c40eb0beafa4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx @@ -24,16 +24,12 @@ import { import { i18n } from '@kbn/i18n'; import { Status } from '../../../../../../common/types/api'; -import { DependencyLookup, DisplayType } from '../../../../../../common/types/connectors'; +import { DisplayType } from '../../../../../../common/types/connectors'; import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; import { ConnectorConfigurationField } from './connector_configuration_field'; -import { - ConfigEntry, - ConnectorConfigurationLogic, - dependenciesSatisfied, -} from './connector_configuration_logic'; +import { ConnectorConfigurationLogic } from './connector_configuration_logic'; export const ConnectorConfigurationForm = () => { const { status } = useValues(ConnectorConfigurationApiLogic); @@ -41,20 +37,6 @@ export const ConnectorConfigurationForm = () => { const { localConfigView } = useValues(ConnectorConfigurationLogic); const { saveConfig, setIsEditing } = useActions(ConnectorConfigurationLogic); - const dependencyLookup: DependencyLookup = localConfigView.reduce( - (prev: Record, configEntry: ConfigEntry) => ({ - ...prev, - [configEntry.key]: configEntry.value, - }), - {} - ); - - const filteredConfigView = localConfigView.filter( - (configEntry) => - configEntry.ui_restrictions.length <= 0 && - dependenciesSatisfied(configEntry.depends_on, dependencyLookup) - ); - return ( { @@ -63,7 +45,7 @@ export const ConnectorConfigurationForm = () => { }} component="form" > - {filteredConfigView.map((configEntry, index) => { + {localConfigView.map((configEntry, index) => { const { default_value: defaultValue, depends_on: dependencies, @@ -94,8 +76,8 @@ export const ConnectorConfigurationForm = () => { if (dependencies.length > 0) { // dynamic spacing without CSS - const previousField = filteredConfigView[index - 1]; - const nextField = filteredConfigView[index + 1]; + const previousField = localConfigView[index - 1]; + const nextField = localConfigView[index + 1]; const topSpacing = !previousField || previousField.depends_on.length <= 0 ? : <>; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts index 64e7d1af9c999..f87d73b882ecd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts @@ -154,7 +154,7 @@ describe('ConnectorConfigurationLogic', () => { }); }); describe('setLocalConfigEntry', () => { - it('should set local config entry and sort keys', () => { + it('should set local config entry, and sort and filter keys', () => { ConnectorConfigurationLogic.actions.setConfigState({ bar: { default_value: '', @@ -182,6 +182,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigState({ bar: { @@ -210,6 +281,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigEntry({ default_value: '', @@ -254,6 +396,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, configView: [ { @@ -284,6 +497,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + key: 'shownDependent1', + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'shownDependent2', + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, ], localConfigState: { bar: { @@ -312,6 +556,77 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, localConfigView: [ { @@ -342,6 +657,37 @@ describe('ConnectorConfigurationLogic', () => { ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent1', + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent2', + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, ], }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts index 84b0fd4d23fdb..861ab90079229 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts @@ -72,12 +72,17 @@ export interface ConfigEntry { /** * - * Sorts the connector configuration by specified order (if present) + * Sorts and filters the connector configuration + * + * Sorting is done by specified order (if present) * otherwise by alphabetic order of keys * + * Filtering is done on any fields with ui_restrictions + * or that have not had their dependencies met + * */ -function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { - return Object.keys(config) +function sortAndFilterConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { + const sortedConfig = Object.keys(config) .map( (key) => ({ @@ -98,6 +103,20 @@ function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry } return a.key.localeCompare(b.key); }); + + const dependencyLookup: DependencyLookup = sortedConfig.reduce( + (prev: Record, configEntry: ConfigEntry) => ({ + ...prev, + [configEntry.key]: configEntry.value, + }), + {} + ); + + return sortedConfig.filter( + (configEntry) => + configEntry.ui_restrictions.length <= 0 && + dependenciesSatisfied(configEntry.depends_on, dependencyLookup) + ); } export function ensureStringType(value: string | number | boolean | null): string { @@ -280,11 +299,11 @@ export const ConnectorConfigurationLogic = kea< selectors: ({ selectors }) => ({ configView: [ () => [selectors.configState], - (configState: ConnectorConfiguration) => sortConnectorConfiguration(configState), + (configState: ConnectorConfiguration) => sortAndFilterConnectorConfiguration(configState), ], localConfigView: [ () => [selectors.localConfigState], - (configState) => sortConnectorConfiguration(configState), + (configState) => sortAndFilterConnectorConfiguration(configState), ], }), }); From 54457b074a20da8017de03feb9ebfbe0fe6450d3 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 24 Apr 2023 11:13:57 -0300 Subject: [PATCH 09/36] [Infrastructure UI] Plot metric charts data based on current page items (#155249) closes [#152186](https://github.com/elastic/kibana/issues/152186) ## Summary This PR makes the metric charts show data for the hosts on the current page. With this change, the charts will **only** load after the table has finished loading its data - or after Snapshot API has responded It also changes the current behavior of the table pagination and sorting. Instead of relying on the `EuiInMemoryTable` the pagination and sorting are done manually, and the EuiInMemoryTable has been replaced by the `EuiBasicTable`. The loading indicator has also been replaced. Paginating and sorting: https://user-images.githubusercontent.com/2767137/233161166-2bd719e1-7259-4ecc-96a7-50493bc6c0a3.mov Open in lens https://user-images.githubusercontent.com/2767137/233161134-621afd76-44b5-42ab-b58c-7f51ef944ac2.mov ### How to test - Go to Hosts view - Paginate and sort the table data - Select a page size and check if the select has been stored in the localStorage (`hostsView:pageSizeSelection` key) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../hosts/components/chart/chart_loader.tsx | 58 ++++++ .../hosts/components/chart/lens_wrapper.tsx | 87 ++++---- .../components/chart/metric_chart_wrapper.tsx | 53 ++--- .../metadata/metadata.test.tsx | 42 +--- .../hosts/components/hosts_container.tsx | 33 +-- .../metrics/hosts/components/hosts_table.tsx | 127 +++++------- .../hosts/components/kpis/kpi_grid.tsx | 5 +- .../metrics/hosts/components/kpis/tile.tsx | 15 +- .../components/tabs/logs/logs_tab_content.tsx | 5 +- .../components/tabs/metrics/metric_chart.tsx | 63 ++++-- .../public/pages/metrics/hosts/constants.ts | 3 + .../hosts/hooks/use_after_loaded_state.ts | 26 +++ .../metrics/hosts/hooks/use_alerts_query.ts | 2 +- .../hosts/hooks/use_hosts_table.test.ts | 192 +++++++++--------- .../metrics/hosts/hooks/use_hosts_table.tsx | 112 ++++++++-- .../hosts/hooks/use_hosts_table_url_state.ts | 94 +++++++++ .../hooks/use_table_properties_url_state.ts | 62 ------ .../infra/public/pages/metrics/hosts/utils.ts | 17 +- .../test/functional/apps/infra/hosts_view.ts | 83 ++++++++ .../page_objects/infra_hosts_view.ts | 47 +++++ 20 files changed, 716 insertions(+), 410 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx new file mode 100644 index 0000000000000..bbddb338ef73f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; +import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +export const ChartLoader = ({ + children, + loading, + style, + loadedOnce = false, + hasTitle = false, +}: { + style?: React.CSSProperties; + children: React.ReactNode; + loadedOnce: boolean; + loading: boolean; + hasTitle?: boolean; +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + {loading && ( + + )} + {loading && !loadedOnce ? ( + + + + + + ) : ( + children + )} + + ); +}; + +const LoaderContainer = euiStyled.div` + position: relative; + border-radius: ${({ theme }) => theme.eui.euiSizeS}; + overflow: hidden; + height: 100%; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9985db0751fd4..9a2472949f54c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -4,18 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiLoadingChart } from '@elastic/eui'; import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; +import { ChartLoader } from './chart_loader'; export interface Props { id: string; @@ -26,7 +24,10 @@ export interface Props { extraActions: Action[]; lastReloadRequestTime?: number; style?: React.CSSProperties; + loading?: boolean; + hasTitle?: boolean; onBrushEnd?: (data: BrushTriggerEvent['data']) => void; + onLoad?: () => void; } export const LensWrapper = ({ @@ -39,12 +40,19 @@ export const LensWrapper = ({ style, onBrushEnd, lastReloadRequestTime, + loading = false, + hasTitle = false, }: Props) => { - const intersectionRef = React.useRef(null); + const intersectionRef = useRef(null); + const [loadedOnce, setLoadedOnce] = useState(false); + + const [state, setState] = useState({ + lastReloadRequestTime, + query, + filters, + dateRange, + }); - const [currentLastReloadRequestTime, setCurrentLastReloadRequestTime] = useState< - number | undefined - >(lastReloadRequestTime); const { services: { lens }, } = useKibanaContextForPlugin(); @@ -56,38 +64,49 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { - setCurrentLastReloadRequestTime(lastReloadRequestTime); + setState({ + lastReloadRequestTime, + query, + dateRange, + filters, + }); } - }, [intersection?.intersectionRatio, lastReloadRequestTime]); + }, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]); const isReady = attributes && intersectedOnce; return (
- {!isReady ? ( - - - - - - ) : ( - - )} + + {isReady && ( + { + if (!loadedOnce) { + setLoadedOnce(true); + } + }} + /> + )} +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx index 9df937983ae1e..8d78906bd03e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx @@ -14,16 +14,11 @@ import { } from '@elastic/charts'; import { EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; -import { EuiLoadingChart } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; -import { EuiProgress } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useEuiTheme } from '@elastic/eui'; import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../../common/http_api'; import { createInventoryMetricFormatter } from '../../../inventory_view/lib/create_inventory_metric_formatter'; import type { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { ChartLoader } from './chart_loader'; type MetricType = keyof Pick; @@ -65,7 +60,6 @@ export const MetricChartWrapper = ({ type, ...props }: Props) => { - const { euiTheme } = useEuiTheme(); const loadedOnce = useRef(false); const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); const metricsTimeseries = useMemo( @@ -109,39 +103,18 @@ export const MetricChartWrapper = ({ return ( -
- {loading && ( - - )} - {loading && !loadedOnce.current ? ( - - - - - - ) : ( - - - - - - )} -
+ + + + + + +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx index 1c6320c142d7a..46392fa8609d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx @@ -32,42 +32,12 @@ const metadataProps: TabProps = { name: 'host-1', cloudProvider: 'gcp', }, - rx: { - name: 'rx', - value: 0, - max: 0, - avg: 0, - }, - tx: { - name: 'tx', - value: 0, - max: 0, - avg: 0, - }, - memory: { - name: 'memory', - value: 0.5445920331099282, - max: 0.5445920331099282, - avg: 0.5445920331099282, - }, - cpu: { - name: 'cpu', - value: 0.2000718443867342, - max: 0.2000718443867342, - avg: 0.2000718443867342, - }, - diskLatency: { - name: 'diskLatency', - value: null, - max: 0, - avg: 0, - }, - memoryTotal: { - name: 'memoryTotal', - value: 16777216, - max: 16777216, - avg: 16777216, - }, + rx: 0, + tx: 0, + memory: 0.5445920331099282, + cpu: 0.2000718443867342, + diskLatency: 0, + memoryTotal: 16777216, }, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index e8e8a8a8e7c4f..0c965feca8e9e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -12,10 +12,11 @@ import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; import { UnifiedSearchBar } from './unified_search_bar'; import { HostsTable } from './hosts_table'; -import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; import { AlertsQueryProvider } from '../hooks/use_alerts_query'; -import { KPIGrid } from './kpis/kpi_grid'; +import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { HostsTableProvider } from '../hooks/use_hosts_table'; export const HostContainer = () => { const { dataView, loading, hasError } = useMetricsDataViewContext(); @@ -38,19 +39,21 @@ export const HostContainer = () => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index ca6f904ceea84..535afe8befff5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -5,93 +5,78 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; import { NoData } from '../../../../components/empty_states'; -import { InfraLoadingPanel } from '../../../../components/loading'; -import { useHostsTable } from '../hooks/use_hosts_table'; -import { useTableProperties } from '../hooks/use_table_properties_url_state'; +import { HostNodeRow, useHostsTableContext } from '../hooks/use_hosts_table'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; import { Flyout } from './host_details_flyout/flyout'; +import { DEFAULT_PAGE_SIZE } from '../constants'; -export const HostsTable = () => { - const { hostNodes, loading } = useHostsViewContext(); - const { onSubmit, searchCriteria } = useUnifiedSearchContext(); - const [properties, setProperties] = useTableProperties(); - - const { columns, items, isFlyoutOpen, closeFlyout, clickedItem } = useHostsTable(hostNodes, { - time: searchCriteria.dateRange, - }); - - const noData = items.length === 0; - - const onTableChange = useCallback( - ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field, direction } = sort; - - const sorting = field && direction ? { field, direction } : true; - const pagination = pageIndex >= 0 && pageSize !== 0 ? { pageIndex, pageSize } : true; - - if (!isEqual(properties.sorting, sorting)) { - setProperties({ sorting }); - } - if (!isEqual(properties.pagination, pagination)) { - setProperties({ pagination }); - } - }, - [setProperties, properties.pagination, properties.sorting] - ); +const PAGE_SIZE_OPTIONS = [5, 10, 20]; - if (loading) { - return ( - - ); - } +export const HostsTable = () => { + const { loading } = useHostsViewContext(); + const { onSubmit } = useUnifiedSearchContext(); - if (noData) { - return ( - onSubmit()} - testString="noMetricsDataPrompt" - /> - ); - } + const { + columns, + items, + currentPage, + isFlyoutOpen, + closeFlyout, + clickedItem, + onTableChange, + pagination, + sorting, + } = useHostsTableContext(); return ( <> - onSubmit()} + testString="noMetricsDataPrompt" + /> + ) + } /> {isFlyoutOpen && clickedItem && } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 968e7462b38f4..2dbd0c4324eca 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -6,11 +6,8 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { KPIChartProps, Tile } from './tile'; import { HostsTile } from './hosts_tile'; import { ChartBaseProps } from '../chart/metric_chart_wrapper'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 480e6c415dc45..a95f18b4a10ee 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -8,13 +8,16 @@ import React from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiI18n, + EuiToolTip, +} from '@elastic/eui'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 0fad370960f22..d5cc0b0f021d7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -24,7 +24,10 @@ export const LogsTabContent = () => { const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo(() => createHostsFilter(hostNodes), [hostNodes]); + const hostsFilterQuery = useMemo( + () => createHostsFilter(hostNodes.map((p) => p.name)), + [hostNodes] + ); const logsLinkToStreamQuery = useMemo(() => { const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 252bea5389e3a..28d07b94d9437 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -4,20 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiI18n, + EuiFlexGroup, + EuiFlexItem, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { HostsLensLineChartFormulas } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; +import { createHostsFilter } from '../../../utils'; +import { useHostsTableContext } from '../../../hooks/use_hosts_table'; import { LensWrapper } from '../../chart/lens_wrapper'; +import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state'; export interface MetricChartProps { title: string; @@ -29,9 +37,18 @@ export interface MetricChartProps { const MIN_HEIGHT = 300; export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => { + const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { baseRequest, loading } = useHostsViewContext(); + const { currentPage } = useHostsTableContext(); + + // prevents updates on requestTs and serchCriteria states from relaoding the chart + // we want it to reload only once the table has finished loading + const { afterLoadedState } = useAfterLoadedState(loading, { + lastReloadRequestTime: baseRequest.requestTs, + ...searchCriteria, + }); const { attributes, getExtraActions, error } = useLensAttributes({ type, @@ -43,11 +60,22 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => visualizationType: 'lineChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + currentPage.map((p) => p.name), + dataView + ); + }, [currentPage, dataView]); + + const filters = [ + ...afterLoadedState.filters, + ...afterLoadedState.panelFilters, + ...[hostsFilterQuery], + ]; const extraActionOptions = getExtraActions({ - timeRange: searchCriteria.dateRange, + timeRange: afterLoadedState.dateRange, filters, - query: searchCriteria.query, + query: afterLoadedState.query, }); const extraActions: Action[] = [extraActionOptions.openInLens]; @@ -69,12 +97,15 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => hasShadow={false} hasBorder paddingSize={error ? 'm' : 'none'} - style={{ minHeight: MIN_HEIGHT }} + css={css` + min-height: calc(${MIN_HEIGHT} + ${euiTheme.size.l}); + position: 'relative'; + `} data-test-subj={`hostsView-metricChart-${type}`} > {error ? ( attributes={attributes} style={{ height: MIN_HEIGHT }} extraActions={extraActions} - lastReloadRequestTime={baseRequest.requestTs} - dateRange={searchCriteria.dateRange} + lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} + dateRange={afterLoadedState.dateRange} filters={filters} - query={searchCriteria.query} + query={afterLoadedState.query} onBrushEnd={handleBrushEnd} + loading={loading} + hasTitle /> )} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 98aa8a145e3a0..b854120a86887 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -13,6 +13,9 @@ export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; + export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, label: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.showAll', { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts new file mode 100644 index 0000000000000..8c9a84d4402f8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts @@ -0,0 +1,26 @@ +/* + * 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 { useState, useEffect, useRef } from 'react'; + +export const useAfterLoadedState = (loading: boolean, state: T) => { + const ref = useRef(undefined); + const [internalState, setInternalState] = useState(state); + + if (!ref.current || loading !== ref.current) { + ref.current = loading; + } + + useEffect(() => { + if (!loading) { + setInternalState(state); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref.current]); + + return { afterLoadedState: internalState }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 9877d61643721..7a895591d68c7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -69,7 +69,7 @@ const createAlertsEsQuery = ({ const alertStatusFilter = createAlertStatusFilter(status); const dateFilter = createDateFilter(dateRange); - const hostsFilter = createHostsFilter(hostNodes); + const hostsFilter = createHostsFilter(hostNodes.map((p) => p.name)); const filters = [alertStatusFilter, dateFilter, hostsFilter].filter(Boolean) as Filter[]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index 4ae8823adaf2e..a921a0daeb011 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -8,68 +8,92 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; import { SnapshotNode } from '../../../../../common/http_api'; +import * as useUnifiedSearchHooks from './use_unified_search'; +import * as useHostsViewHooks from './use_hosts_view'; -describe('useHostTable hook', () => { - it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { - const nodes: SnapshotNode[] = [ +jest.mock('./use_unified_search'); +jest.mock('./use_hosts_view'); + +const mockUseUnifiedSearchContext = + useUnifiedSearchHooks.useUnifiedSearchContext as jest.MockedFunction< + typeof useUnifiedSearchHooks.useUnifiedSearchContext + >; +const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.MockedFunction< + typeof useHostsViewHooks.useHostsViewContext +>; + +const mockHostNode: SnapshotNode[] = [ + { + metrics: [ { - metrics: [ - { - name: 'rx', - avg: 252456.92916666667, - }, - { - name: 'tx', - avg: 252758.425, - }, - { - name: 'memory', - avg: 0.94525, - }, - { - name: 'cpu', - value: 0.6353277777777777, - }, - { - name: 'memoryTotal', - avg: 34359.738368, - }, - ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], - name: 'host-0', + name: 'rx', + avg: 252456.92916666667, }, { - metrics: [ - { - name: 'rx', - avg: 95.86339715321859, - }, - { - name: 'tx', - avg: 110.38566859563191, - }, - { - name: 'memory', - avg: 0.5400000214576721, - }, - { - name: 'cpu', - value: 0.8647805555555556, - }, - { - name: 'memoryTotal', - avg: 9.194304, - }, - ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, - ], - name: 'host-1', + name: 'tx', + avg: 252758.425, }, - ]; + { + name: 'memory', + avg: 0.94525, + }, + { + name: 'cpu', + value: 0.6353277777777777, + }, + { + name: 'memoryTotal', + avg: 34359.738368, + }, + ], + path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], + name: 'host-0', + }, + { + metrics: [ + { + name: 'rx', + avg: 95.86339715321859, + }, + { + name: 'tx', + avg: 110.38566859563191, + }, + { + name: 'memory', + avg: 0.5400000214576721, + }, + { + name: 'cpu', + value: 0.8647805555555556, + }, + { + name: 'memoryTotal', + avg: 9.194304, + }, + ], + path: [ + { value: 'host-1', label: 'host-1' }, + { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, + ], + name: 'host-1', + }, +]; + +describe('useHostTable hook', () => { + beforeAll(() => { + mockUseUnifiedSearchContext.mockReturnValue({ + searchCriteria: { + dateRange: { from: 'now-15m', to: 'now' }, + }, + } as ReturnType); - const items = [ + mockUseHostsViewContext.mockReturnValue({ + hostNodes: mockHostNode, + } as ReturnType); + }); + it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { + const expected = [ { name: 'host-0', os: '-', @@ -79,27 +103,11 @@ describe('useHostTable hook', () => { cloudProvider: 'aws', name: 'host-0', }, - rx: { - name: 'rx', - avg: 252456.92916666667, - }, - tx: { - name: 'tx', - avg: 252758.425, - }, - memory: { - name: 'memory', - avg: 0.94525, - }, - cpu: { - name: 'cpu', - value: 0.6353277777777777, - }, - memoryTotal: { - name: 'memoryTotal', - - avg: 34359.738368, - }, + rx: 252456.92916666667, + tx: 252758.425, + memory: 0.94525, + cpu: 0.6353277777777777, + memoryTotal: 34359.738368, }, { name: 'host-1', @@ -110,32 +118,16 @@ describe('useHostTable hook', () => { cloudProvider: null, name: 'host-1', }, - rx: { - name: 'rx', - avg: 95.86339715321859, - }, - tx: { - name: 'tx', - avg: 110.38566859563191, - }, - memory: { - name: 'memory', - avg: 0.5400000214576721, - }, - cpu: { - name: 'cpu', - value: 0.8647805555555556, - }, - memoryTotal: { - name: 'memoryTotal', - avg: 9.194304, - }, + rx: 95.86339715321859, + tx: 110.38566859563191, + memory: 0.5400000214576721, + cpu: 0.8647805555555556, + memoryTotal: 9.194304, }, ]; - const time = { from: 'now-15m', to: 'now', interval: '>=1m' }; - const { result } = renderHook(() => useHostsTable(nodes, { time })); + const { result } = renderHook(() => useHostsTable()); - expect(result.current.items).toStrictEqual(items); + expect(result.current.items).toStrictEqual(expected); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 44a492f314c1c..2d2d6c9d7f8e4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -8,8 +8,10 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; - +import createContainer from 'constate'; +import { isEqual } from 'lodash'; +import { CriteriaWithPagination } from '@elastic/eui'; +import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; @@ -19,6 +21,9 @@ import type { SnapshotMetricInput, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; +import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; +import { useHostsViewContext } from './use_hosts_view'; +import { useUnifiedSearchContext } from './use_unified_search'; /** * Columns and items types @@ -27,7 +32,7 @@ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; -type HostMetrics = Record; +type HostMetrics = Record; export interface HostNodeRow extends HostMetrics { os?: string | null; @@ -38,10 +43,6 @@ export interface HostNodeRow extends HostMetrics { id: string; } -interface HostTableParams { - time: TimeRange; -} - /** * Helper functions */ @@ -60,12 +61,41 @@ const buildItemsList = (nodes: SnapshotNode[]) => { cloudProvider: path.at(-1)?.cloudProvider ?? null, }, ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric; + data[metric.name as HostMetric] = metric.avg ?? metric.value; return data; }, {} as HostMetrics), })) as HostNodeRow[]; }; +const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { + return typeof cell === 'object' && cell && 'name' in cell; +}; + +const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => { + if (typeof aValue === 'string' && typeof bValue === 'string') { + return direction === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + } + + if (isNumber(aValue) && isNumber(bValue)) { + return direction === 'desc' ? bValue - aValue : aValue - bValue; + } + + return 1; +}; + +const sortTableData = + ({ direction, field }: Sorting) => + (a: HostNodeRow, b: HostNodeRow) => { + const aValue = a[field as keyof HostNodeRow]; + const bValue = b[field as keyof HostNodeRow]; + + if (isTitleColumn(aValue) && isTitleColumn(bValue)) { + return sortValues(aValue.name, bValue.name, { direction, field }); + } + + return sortValues(aValue, bValue, { direction, field }); + }; + /** * Columns translations */ @@ -120,7 +150,10 @@ const toggleDialogActionLabel = i18n.translate( /** * Build a table columns and items starting from the snapshot nodes. */ -export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) => { +export const useHostsTable = () => { + const { hostNodes } = useHostsViewContext(); + const { searchCriteria } = useUnifiedSearchContext(); + const [{ pagination, sorting }, setProperties] = useHostsTableProperties(); const { services: { telemetry }, } = useKibanaContextForPlugin(); @@ -139,12 +172,38 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) [telemetry] ); - const items = useMemo(() => buildItemsList(nodes), [nodes]); + const onTableChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort ?? {}; + + const currentSorting = { field: field as keyof HostNodeRow, direction }; + const currentPagination = { pageIndex, pageSize }; + + if (!isEqual(sorting, currentSorting)) { + setProperties({ sorting: currentSorting }); + } else if (!isEqual(pagination, currentPagination)) { + setProperties({ pagination: currentPagination }); + } + }, + [setProperties, pagination, sorting] + ); + + const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]); const clickedItem = useMemo( () => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId), [hostFlyoutOpen.clickedItemId, items] ); + const currentPage = useMemo(() => { + const { pageSize = 0, pageIndex = 0 } = pagination; + + const endIndex = (pageIndex + 1) * pageSize; + const startIndex = pageIndex * pageSize; + + return items.sort(sortTableData(sorting)).slice(startIndex, endIndex); + }, [items, pagination, sorting]); + const columns: Array> = useMemo( () => [ { @@ -183,7 +242,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) render: (title: HostNodeRow['title']) => ( reportHostEntryClick(title)} /> ), @@ -197,7 +256,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageCpuUsageLabel, - field: 'cpu.avg', + field: 'cpu', sortable: true, 'data-test-subj': 'hostsView-tableRow-cpuUsage', render: (avg: number) => formatMetric('cpu', avg), @@ -205,7 +264,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: diskLatencyLabel, - field: 'diskLatency.avg', + field: 'diskLatency', sortable: true, 'data-test-subj': 'hostsView-tableRow-diskLatency', render: (avg: number) => formatMetric('diskLatency', avg), @@ -213,7 +272,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageRXLabel, - field: 'rx.avg', + field: 'rx', sortable: true, 'data-test-subj': 'hostsView-tableRow-rx', render: (avg: number) => formatMetric('rx', avg), @@ -221,7 +280,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTXLabel, - field: 'tx.avg', + field: 'tx', sortable: true, 'data-test-subj': 'hostsView-tableRow-tx', render: (avg: number) => formatMetric('tx', avg), @@ -229,7 +288,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTotalMemoryLabel, - field: 'memoryTotal.avg', + field: 'memoryTotal', sortable: true, 'data-test-subj': 'hostsView-tableRow-memoryTotal', render: (avg: number) => formatMetric('memoryTotal', avg), @@ -237,21 +296,34 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageMemoryUsageLabel, - field: 'memory.avg', + field: 'memory', sortable: true, 'data-test-subj': 'hostsView-tableRow-memory', render: (avg: number) => formatMetric('memory', avg), align: 'right', }, ], - [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setFlyoutClosed, setHostFlyoutOpen, time] + [ + hostFlyoutOpen.clickedItemId, + reportHostEntryClick, + searchCriteria.dateRange, + setFlyoutClosed, + setHostFlyoutOpen, + ] ); return { columns, - items, clickedItem, - isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + currentPage, closeFlyout, + items, + isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + onTableChange, + pagination, + sorting, }; }; + +export const HostsTable = createContainer(useHostsTable); +export const [HostsTableProvider, useHostsTableContext] = HostsTable; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts new file mode 100644 index 0000000000000..b4889d62f5878 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts @@ -0,0 +1,94 @@ +/* + * 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 * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import deepEqual from 'fast-deep-equal'; +import { useReducer } from 'react'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants'; + +export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { + sorting: { + direction: 'asc', + field: 'name', + }, + pagination: { + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + }, +}; + +const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; + +const reducer = (prevState: TableProperties, params: Payload) => { + const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v)); + + return { + ...prevState, + ...payload, + }; +}; + +export const useHostsTableProperties = (): [TableProperties, TablePropertiesUpdater] => { + const [localStoragePageSize, setLocalStoragePageSize] = useLocalStorage( + LOCAL_STORAGE_PAGE_SIZE_KEY, + DEFAULT_PAGE_SIZE + ); + + const [urlState, setUrlState] = useUrlState({ + defaultState: { + ...GET_DEFAULT_TABLE_PROPERTIES, + pagination: { + ...GET_DEFAULT_TABLE_PROPERTIES.pagination, + pageSize: localStoragePageSize, + }, + }, + + decodeUrlState, + encodeUrlState, + urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, + }); + + const [properties, setProperties] = useReducer(reducer, urlState); + if (!deepEqual(properties, urlState)) { + setUrlState(properties); + if (localStoragePageSize !== properties.pagination.pageSize) { + setLocalStoragePageSize(properties.pagination.pageSize); + } + } + + return [properties, setProperties]; +}; + +const PaginationRT = rt.partial({ pageIndex: rt.number, pageSize: rt.number }); +const SortingRT = rt.intersection([ + rt.type({ + field: rt.string, + }), + rt.partial({ direction: rt.union([rt.literal('asc'), rt.literal('desc')]) }), +]); + +const TableStateRT = rt.type({ + pagination: PaginationRT, + sorting: SortingRT, +}); + +export type TableState = rt.TypeOf; +export type Payload = Partial; +export type TablePropertiesUpdater = (params: Payload) => void; + +export type Sorting = rt.TypeOf; +type TableProperties = rt.TypeOf; + +const encodeUrlState = TableStateRT.encode; +const decodeUrlState = (value: unknown) => { + return pipe(TableStateRT.decode(value), fold(constant(undefined), identity)); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts deleted file mode 100644 index 980fdf19a684c..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 * as rt from 'io-ts'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../../utils/use_url_state'; - -export const GET_DEFAULT_TABLE_PROPERTIES = { - sorting: true, - pagination: true, -}; -const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; - -type Action = rt.TypeOf; -type PropertiesUpdater = (newProps: Action) => void; - -export const useTableProperties = (): [TableProperties, PropertiesUpdater] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, - decodeUrlState, - encodeUrlState, - urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, - }); - - const setProperties = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); - - return [urlState, setProperties]; -}; - -const PaginationRT = rt.union([ - rt.boolean, - rt.partial({ pageIndex: rt.number, pageSize: rt.number }), -]); -const SortingRT = rt.union([rt.boolean, rt.type({ field: rt.string, direction: rt.any })]); - -const SetSortingRT = rt.partial({ - sorting: SortingRT, -}); - -const SetPaginationRT = rt.partial({ - pagination: PaginationRT, -}); - -const ActionRT = rt.intersection([SetSortingRT, SetPaginationRT]); - -const TablePropertiesRT = rt.type({ - pagination: PaginationRT, - sorting: SortingRT, -}); - -type TableProperties = rt.TypeOf; - -const encodeUrlState = TablePropertiesRT.encode; -const decodeUrlState = (value: unknown) => { - return pipe(TablePropertiesRT.decode(value), fold(constant(undefined), identity)); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts index a04fdfa46b279..5da9d36b0f587 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts @@ -5,16 +5,23 @@ * 2.0. */ -import { Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../common/http_api'; +import { DataViewBase, Filter } from '@kbn/es-query'; -export const createHostsFilter = (hostNodes: SnapshotNode[]): Filter => { +export const createHostsFilter = (hostNames: string[], dataView?: DataViewBase): Filter => { return { query: { terms: { - 'host.name': hostNodes.map((p) => p.name), + 'host.name': hostNames, }, }, - meta: {}, + meta: dataView + ? { + value: hostNames.join(), + type: 'phrases', + params: hostNames, + index: dataView.id, + key: 'host.name', + } + : {}, }; }; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 3cf0091c93bd4..e9000a9cf3e6d 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -529,6 +529,89 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Pagination and Sorting', () => { + beforeEach(async () => { + await pageObjects.infraHostsView.changePageSize(5); + }); + + it('should show 5 rows on the first page', async () => { + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should paginate to the last page', async () => { + await pageObjects.infraHostsView.paginateTo(2); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[5])); + }); + }); + + it('should show all hosts on the same page', async () => { + await pageObjects.infraHostsView.changePageSize(10); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should sort by Disk Latency asc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[1]); + }); + + it('should sort by Disk Latency desc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[1]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + + it('should sort by Title asc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[5]); + }); + + it('should sort by Title desc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[5]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + }); }); }); }; diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index ae0cc601f8cc7..6478d208226ad 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -241,6 +241,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { async typeInQueryBar(query: string) { const queryBar = await this.getQueryBar(); + await queryBar.clearValueWithKeyboard(); return queryBar.type(query); }, @@ -249,5 +250,51 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); }, + + // Pagination + getPageNumberButton(pageNumber: number) { + return testSubjects.find(`pagination-button-${pageNumber - 1}`); + }, + + getPageSizeSelector() { + return testSubjects.find('tablePaginationPopoverButton'); + }, + + getPageSizeOption(pageSize: number) { + return testSubjects.find(`tablePagination-${pageSize}-rows`); + }, + + async changePageSize(pageSize: number) { + const pageSizeSelector = await this.getPageSizeSelector(); + await pageSizeSelector.click(); + const pageSizeOption = await this.getPageSizeOption(pageSize); + await pageSizeOption.click(); + }, + + async paginateTo(pageNumber: number) { + const paginationButton = await this.getPageNumberButton(pageNumber); + await paginationButton.click(); + }, + + // Sorting + getDiskLatencyHeader() { + return testSubjects.find('tableHeaderCell_diskLatency_4'); + }, + + getTitleHeader() { + return testSubjects.find('tableHeaderCell_title_1'); + }, + + async sortByDiskLatency() { + const diskLatency = await this.getDiskLatencyHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', diskLatency); + return button.click(); + }, + + async sortByTitle() { + const titleHeader = await this.getTitleHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', titleHeader); + return button.click(); + }, }; } From 111d04f45a64cc050407bd9f892e1f77ddd8cc9f Mon Sep 17 00:00:00 2001 From: Katerina Patticha Date: Mon, 24 Apr 2023 16:27:38 +0200 Subject: [PATCH 10/36] [APM] Add transaction name filter in failed transaction rate rule type (#155405) part of https://github.com/elastic/kibana/issues/152329 related work https://github.com/elastic/kibana/pull/154241 Introduces the Transaction name filter in the failed transaction rate rule type https://user-images.githubusercontent.com/3369346/233386404-1875b283-0321-4bf1-a7d3-66327f7d4ec5.mov ## Fixes The regression introduces in a previous [PR](https://github.com/elastic/kibana/pull/154241/commits/fce4ef8168429645a01434e19b0feaefba1a4f02) Existing rule types can have empty string in their params so we need to make sure we don't filter empty values as it will yield no results. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/common/rules/schema.ts | 1 + .../index.stories.tsx | 105 ++++++++++++++++++ .../index.tsx | 16 ++- .../register_error_count_rule_type.ts | 8 +- ...register_transaction_duration_rule_type.ts | 12 +- ...et_transaction_error_rate_chart_preview.ts | 13 ++- ...gister_transaction_error_rate_rule_type.ts | 16 ++- .../tests/alerts/chart_preview.spec.ts | 55 +++++++++ 8 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts index 698b4507c5b3f..ca77e76f6f156 100644 --- a/x-pack/plugins/apm/common/rules/schema.ts +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -52,6 +52,7 @@ export const transactionErrorRateParamsSchema = schema.object({ windowUnit: schema.string(), threshold: schema.number(), transactionType: schema.maybe(schema.string()), + transactionName: schema.maybe(schema.string()), serviceName: schema.maybe(schema.string()), environment: schema.string(), }); diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx new file mode 100644 index 0000000000000..cd94439db0389 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx @@ -0,0 +1,105 @@ +/* + * 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 { Story } from '@storybook/react'; +import React, { ComponentType, useState } from 'react'; +import { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { RuleParams, TransactionErrorRateRuleType } from '.'; +import { AlertMetadata } from '../../utils/helper'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + ruleParams: RuleParams; + metadata?: AlertMetadata; +} + +export default { + title: 'alerting/TransactionErrorRateRuleType', + component: TransactionErrorRateRuleType, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + +
+ +
+
+ ); + }, + ], +}; + +export const CreatingInApmServiceOverview: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInApmServiceOverview.args = { + ruleParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 1500, + transactionType: 'testTransactionType', + transactionName: 'GET /api/customer/:id', + windowSize: 5, + windowUnit: 'm', + }, + metadata: { + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + }, +}; + +export const CreatingInStackManagement: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInStackManagement.args = { + ruleParams: { + environment: 'testEnvironment', + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + }, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx index f9cfd6a511ef2..f161ef085b3ea 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx @@ -23,20 +23,22 @@ import { IsAboveField, ServiceField, TransactionTypeField, + TransactionNameField, } from '../../utils/fields'; import { AlertMetadata, getIntervalAndTimeRange } from '../../utils/helper'; import { ApmRuleParamsContainer } from '../../ui_components/apm_rule_params_container'; -interface RuleParams { +export interface RuleParams { windowSize?: number; windowUnit?: string; threshold?: number; serviceName?: string; transactionType?: string; + transactionName?: string; environment?: string; } -interface Props { +export interface Props { ruleParams: RuleParams; metadata?: AlertMetadata; setRuleParams: (key: string, value: any) => void; @@ -78,6 +80,7 @@ export function TransactionErrorRateRuleType(props: Props) { environment: params.environment, serviceName: params.serviceName, transactionType: params.transactionType, + transactionName: params.transactionName, interval, start, end, @@ -89,6 +92,7 @@ export function TransactionErrorRateRuleType(props: Props) { }, [ params.transactionType, + params.transactionName, params.environment, params.serviceName, params.windowSize, @@ -102,7 +106,8 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => { if (value !== params.serviceName) { setRuleParams('serviceName', value); - setRuleParams('transactionType', ''); + setRuleParams('transactionType', undefined); + setRuleParams('transactionName', undefined); setRuleParams('environment', ENVIRONMENT_ALL.value); } }} @@ -117,6 +122,11 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => setRuleParams('environment', value)} serviceName={params.serviceName} />, + setRuleParams('transactionName', value)} + serviceName={params.serviceName} + />, { - const { serviceName, environment, transactionType, interval, start, end } = - alertParams; + const { + serviceName, + environment, + transactionType, + interval, + start, + end, + transactionName, + } = alertParams; const searchAggregatedTransactions = await getSearchTransactionsEvents({ config, @@ -62,6 +70,7 @@ export async function getTransactionErrorRateChartPreview({ filter: [ ...termQuery(SERVICE_NAME, serviceName), ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 7ceaf8ca78048..26b5847a205f1 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -32,6 +32,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, + TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; import { @@ -86,6 +87,7 @@ export function registerTransactionErrorRateRuleType({ apmActionVariables.interval, apmActionVariables.reason, apmActionVariables.serviceName, + apmActionVariables.transactionName, apmActionVariables.threshold, apmActionVariables.transactionType, apmActionVariables.triggerValue, @@ -142,8 +144,15 @@ export function registerTransactionErrorRateRuleType({ ], }, }, - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, ruleParams.transactionName, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ], }, @@ -232,6 +241,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, transactionType, environment, + ruleParams.transactionName, ] .filter((name) => name) .join('_'); @@ -255,6 +265,7 @@ export function registerTransactionErrorRateRuleType({ [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, + [TRANSACTION_NAME]: ruleParams.transactionName, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: errorRate, [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, @@ -272,6 +283,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, threshold: ruleParams.threshold, transactionType, + transactionName: ruleParams.transactionName, triggerValue: asDecimalOrInteger(errorRate), viewInAppUrl, }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts index 7ec09849b7ff2..f95bb8de59a89 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts @@ -83,6 +83,61 @@ export default function ApiTest({ getService }: FtrProviderContext) { ).to.equal(true); }); + it('transaction_error_rate with transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'APIRestController#product', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview[0]).to.eql({ + x: 1627974600000, + y: 1, + }); + }); + + it('transaction_error_rate with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.every( + (item: { x: number; y: number | null }) => item.y === null + ) + ).to.equal(true); + }); + it('error_count (with data)', async () => { const options = getOptions(); options.params.query.transactionType = undefined; From 1095375fe39e960d0569d818191bef468c93a44b Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 24 Apr 2023 16:42:37 +0200 Subject: [PATCH 11/36] [Cases] Close FilePreview with Escape key. (#155592) Fixes #155036 ## Summary Allow users to close the file preview in cases by using the Escape key. (e2e coming in a different PR with other tests) --- .../components/files/file_preview.test.tsx | 20 +++++++++++++++++++ .../public/components/files/file_preview.tsx | 18 +++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx index b02df3a82228f..c1d7fe20bee48 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; @@ -35,4 +36,23 @@ describe('FilePreview', () => { expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); }); + + it('pressing escape calls closePreview', async () => { + const closePreview = jest.fn(); + + appMockRender.render(); + + await waitFor(() => + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + + userEvent.keyboard('{esc}'); + + await waitFor(() => expect(closePreview).toHaveBeenCalled()); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx index 1bb91c5b53ff7..09cee1320ec2a 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { EuiOverlayMask, EuiFocusTrap, EuiImage, keys } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import type { Owner } from '../../../common/constants/types'; @@ -36,6 +36,20 @@ export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => const { client: filesClient } = useFilesContext(); const { owner } = useCasesContext(); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.key === keys.ESCAPE || event.code === 'Escape') { + closePreview(); + } + }; + + window.addEventListener('keyup', keyboardListener); + + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [closePreview]); + return ( From a03d20be039d1c449b2848f46463bc423b6f5183 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:51:36 +0100 Subject: [PATCH 12/36] skip flaky suite (#154970) --- .../sections/alerts_table/bulk_actions/bulk_actions.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index f9d209549da0c..23fac59fca208 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -691,7 +691,8 @@ describe('AlertsTable.BulkActions', () => { ).toBeTruthy(); }); - describe('and clear the selection is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/154970 + describe.skip('and clear the selection is clicked', () => { it('should turn off the toolbar', async () => { const props = { ...tablePropsWithBulkActions, From 2c14b584f8f736f65211b5f738f9e0d764681346 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 24 Apr 2023 15:55:40 +0100 Subject: [PATCH 13/36] skip flaky suite (#155222) --- x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 92320dad62087..54b1baae454bd 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -209,7 +209,8 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { }); } - describe('explain log rate spikes', async function () { + // FLAKY: https://github.com/elastic/kibana/issues/155222 + describe.skip('explain log rate spikes', async function () { for (const testData of explainLogRateSpikesTestData) { describe(`with '${testData.sourceIndexOrSavedSearch}'`, function () { before(async () => { From 3d78370aa584e179ae9e9d30fabe080242812d22 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Mon, 24 Apr 2023 11:02:43 -0400 Subject: [PATCH 14/36] Fix API links when generating API key snippet (#155435) Fixes the Search Applications API page to set an URL to the ES plugin rather than Enterprise Search URL. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../engine_connect/engine_api_integration.tsx | 19 ++++++++++--------- .../engine_connect/search_application_api.tsx | 14 +++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx index b61614838d7a1..2fe691e262b64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx @@ -12,23 +12,24 @@ import { useValues } from 'kea'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; - +import { useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; import { EngineViewLogic } from '../engine_view_logic'; import { EngineApiLogic } from './engine_api_logic'; -const SearchUISnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` +import { elasticsearchUrl } from './search_application_api'; + +const SearchUISnippet = (esUrl: string, engineName: string, apiKey: string) => `6 import EnginesAPIConnector from "@elastic/search-ui-engines-connector"; const connector = new EnginesAPIConnector({ - host: "${enterpriseSearchUrl}", + host: "${esUrl}", engineName: "${engineName}", apiKey: "${apiKey || ''}" });`; -const cURLSnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` -curl --location --request GET '${enterpriseSearchUrl}/api/engines/${engineName}/_search' \\ +const cURLSnippet = (esUrl: string, engineName: string, apiKey: string) => ` +curl --location --request GET '${esUrl}/${engineName}/_search' \\ --header 'Authorization: apiKey ${apiKey || ''}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ @@ -47,19 +48,19 @@ interface Tab { export const EngineApiIntegrationStage: React.FC = () => { const [selectedTab, setSelectedTab] = React.useState('curl'); const { engineName } = useValues(EngineViewLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { apiKey } = useValues(EngineApiLogic); + const cloudContext = useCloudDetails(); const Tabs: Record = { curl: { - code: cURLSnippet(enterpriseSearchUrl, engineName, apiKey), + code: cURLSnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'bash', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.curlTitle', { defaultMessage: 'cURL', }), }, searchui: { - code: SearchUISnippet(enterpriseSearchUrl, engineName, apiKey), + code: SearchUISnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'javascript', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.searchUITitle', { defaultMessage: 'Search UI', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx index 9d3c27895657f..6934de4051bdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx @@ -23,9 +23,10 @@ import { i18n } from '@kbn/i18n'; import { ANALYTICS_PLUGIN } from '../../../../../../common/constants'; import { COLLECTION_INTEGRATE_PATH } from '../../../../analytics/routes'; +import { CloudDetails, useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; +import { decodeCloudId } from '../../../../shared/decode_cloud_id/decode_cloud_id'; import { docLinks } from '../../../../shared/doc_links'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; import { KibanaLogic } from '../../../../shared/kibana'; import { EngineViewLogic } from '../engine_view_logic'; @@ -34,12 +35,19 @@ import { EngineApiIntegrationStage } from './engine_api_integration'; import { EngineApiLogic } from './engine_api_logic'; import { GenerateEngineApiKeyModal } from './generate_engine_api_key_modal/generate_engine_api_key_modal'; +export const elasticsearchUrl = (cloudContext: CloudDetails): string => { + const defaultUrl = 'https://localhost:9200'; + const url = + (cloudContext.cloudId && decodeCloudId(cloudContext.cloudId)?.elasticsearchUrl) || defaultUrl; + return url; +}; + export const SearchApplicationAPI = () => { const { engineName } = useValues(EngineViewLogic); const { isGenerateModalOpen } = useValues(EngineApiLogic); const { openGenerateModal, closeGenerateModal } = useActions(EngineApiLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { navigateToUrl } = useValues(KibanaLogic); + const cloudContext = useCloudDetails(); const steps = [ { @@ -132,7 +140,7 @@ export const SearchApplicationAPI = () => { - {enterpriseSearchUrl} + {elasticsearchUrl(cloudContext)} From 29a10fddc9af9246f6a329a9aa2018b0a907505d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 24 Apr 2023 16:45:41 +0100 Subject: [PATCH 15/36] [Files] Allow option to disable delete action in mgt UI (#155179) --- .../content-management/table_list/index.ts | 2 +- .../table_list/src/components/table.tsx | 33 +++++- .../table_list/src/index.ts | 2 + .../table_list/src/table_list_view.test.tsx | 105 ++++++++++++++++++ .../table_list/src/table_list_view.tsx | 35 +++++- .../table_list/src/types.ts | 13 +++ .../shared-ux/file/types/base_file_client.ts | 1 + packages/shared-ux/file/types/index.ts | 16 +++ src/plugins/files/public/plugin.ts | 11 +- .../adapters/query_filters.ts | 16 +++ .../server/file_service/file_action_types.ts | 4 + .../integration_tests/file_service.test.ts | 21 +++- src/plugins/files/server/routes/find.ts | 4 +- src/plugins/files_management/public/app.tsx | 28 ++++- .../files_management/public/context.tsx | 11 +- .../files_management/public/i18n_texts.ts | 3 + .../public/mount_management_section.tsx | 8 +- src/plugins/files_management/public/types.ts | 2 + 18 files changed, 294 insertions(+), 21 deletions(-) diff --git a/packages/content-management/table_list/index.ts b/packages/content-management/table_list/index.ts index 532b35450d541..9a608b2d6dda3 100644 --- a/packages/content-management/table_list/index.ts +++ b/packages/content-management/table_list/index.ts @@ -8,5 +8,5 @@ export { TableListView, TableListViewProvider, TableListViewKibanaProvider } from './src'; -export type { UserContentCommonSchema } from './src'; +export type { UserContentCommonSchema, RowActions } from './src'; export type { TableListViewKibanaDependencies } from './src/services'; diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 330eb67be4278..3214e7bf00a72 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -17,7 +17,9 @@ import { SearchFilterConfig, Direction, Query, + type EuiTableSelectionType, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useServices } from '../services'; import type { Action } from '../actions'; @@ -26,6 +28,7 @@ import type { Props as TableListViewProps, UserContentCommonSchema, } from '../table_list_view'; +import type { TableItemsRowActions } from '../types'; import { TableSortSelect } from './table_sort_select'; import { TagFilterPanel } from './tag_filter_panel'; import { useTagFilterPanel } from './use_tag_filter_panel'; @@ -51,6 +54,7 @@ interface Props extends State, TagManageme tableColumns: Array>; hasUpdatedAtMetadata: boolean; deleteItems: TableListViewProps['deleteItems']; + tableItemsRowActions: TableItemsRowActions; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; @@ -70,6 +74,7 @@ export function Table({ entityName, entityNamePlural, tagsToTableItemMap, + tableItemsRowActions, deleteItems, tableCaption, onTableChange, @@ -105,13 +110,32 @@ export function Table({ ); }, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]); - const selection = deleteItems - ? { + const selection = useMemo | undefined>(() => { + if (deleteItems) { + return { onSelectionChange: (obj: T[]) => { dispatch({ type: 'onSelectionChange', data: obj }); }, - } - : undefined; + selectable: (obj) => { + const actions = tableItemsRowActions[obj.id]; + return actions?.delete?.enabled !== false; + }, + selectableMessage: (selectable, obj) => { + if (!selectable) { + const actions = tableItemsRowActions[obj.id]; + return ( + actions?.delete?.reason ?? + i18n.translate('contentManagement.tableList.actionsDisabledLabel', { + defaultMessage: 'Actions disabled for this item', + }) + ); + } + return ''; + }, + initialSelected: [], + }; + } + }, [deleteItems, dispatch, tableItemsRowActions]); const { isPopoverOpen, @@ -214,6 +238,7 @@ export function Table({ data-test-subj="itemsInMemTable" rowHeader="attributes.title" tableCaption={tableCaption} + isSelectable /> ); } diff --git a/packages/content-management/table_list/src/index.ts b/packages/content-management/table_list/src/index.ts index df0d1e22bc106..d1e83d7dd2e93 100644 --- a/packages/content-management/table_list/src/index.ts +++ b/packages/content-management/table_list/src/index.ts @@ -15,3 +15,5 @@ export type { } from './table_list_view'; export { TableListViewProvider, TableListViewKibanaProvider } from './services'; + +export type { RowActions } from './types'; diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 62c83fb5b9454..0245af450fb8a 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -1067,4 +1067,109 @@ describe('TableListView', () => { expect(router?.history.location?.search).toBe('?sort=title&sortdir=desc'); }); }); + + describe('row item actions', () => { + const hits: UserContentCommonSchema[] = [ + { + id: '123', + updatedAt: twoDaysAgo.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 1', + description: 'Item 1 description', + }, + references: [], + }, + { + id: '456', + updatedAt: yesterday.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 2', + description: 'Item 2 description', + }, + references: [], + }, + ]; + + const setupTest = async (props?: Partial) => { + let testBed: TestBed | undefined; + const deleteItems = jest.fn(); + await act(async () => { + testBed = await setup({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + deleteItems, + ...props, + }); + }); + + testBed!.component.update(); + return { testBed: testBed!, deleteItems }; + }; + + test('should allow select items to be deleted', async () => { + const { + testBed: { table, find, exists, component, form }, + deleteItems, + } = await setupTest(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + + expect(tableCellsValues).toEqual([ + ['', 'Item 2Item 2 description', yesterdayToString], // First empty col is the "checkbox" + ['', 'Item 1Item 1 description', twoDaysAgoToString], + ]); + + const selectedHit = hits[1]; + + expect(exists('deleteSelectedItems')).toBe(false); + act(() => { + // Select the second item + form.selectCheckBox(`checkboxSelectRow-${selectedHit.id}`); + }); + component.update(); + // Delete button is now visible + expect(exists('deleteSelectedItems')).toBe(true); + + // Click delete and validate that confirm modal opens + expect(component.exists('.euiModal--confirmation')).toBe(false); + act(() => { + find('deleteSelectedItems').simulate('click'); + }); + component.update(); + expect(component.exists('.euiModal--confirmation')).toBe(true); + + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + expect(deleteItems).toHaveBeenCalledWith([selectedHit]); + }); + + test('should allow to disable the "delete" action for a row', async () => { + const reasonMessage = 'This file cannot be deleted.'; + + const { + testBed: { find }, + } = await setupTest({ + rowItemActions: (obj) => { + if (obj.id === hits[1].id) { + return { + delete: { + enabled: false, + reason: reasonMessage, + }, + }; + } + }, + }); + + const firstCheckBox = find(`checkboxSelectRow-${hits[0].id}`); + const secondCheckBox = find(`checkboxSelectRow-${hits[1].id}`); + + expect(firstCheckBox.props().disabled).toBe(false); + expect(secondCheckBox.props().disabled).toBe(true); + // EUI changes the check "title" from "Select this row" to the reason to disable the checkbox + expect(secondCheckBox.props().title).toBe(reasonMessage); + }); + }); }); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 1612649f80bda..2191a3c9b7eee 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -42,6 +42,7 @@ import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; import { useInRouterContext, useUrlState } from './use_url_state'; +import { RowActions, TableItemsRowActions } from './types'; interface ContentEditorConfig extends Pick { @@ -67,6 +68,11 @@ export interface Props RowActions | undefined; children?: ReactNode | undefined; findItems( searchQuery: string, @@ -241,6 +247,7 @@ function TableListViewComp({ urlStateEnabled = true, customTableColumn, emptyPrompt, + rowItemActions, findItems, createItem, editItem, @@ -580,6 +587,15 @@ function TableListViewComp({ return selectedIds.map((selectedId) => itemsById[selectedId]); }, [selectedIds, itemsById]); + const tableItemsRowActions = useMemo(() => { + return items.reduce((acc, item) => { + return { + ...acc, + [item.id]: rowItemActions ? rowItemActions(item) : undefined, + }; + }, {}); + }, [items, rowItemActions]); + // ------------ // Callbacks // ------------ @@ -854,6 +870,20 @@ function TableListViewComp({ }; }, []); + const PageTemplate = useMemo(() => { + return withoutPageTemplateWrapper + ? ((({ + children: _children, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + ['data-test-subj']?: string; + }) => ( +
{_children}
+ )) as unknown as typeof KibanaPageTemplate) + : KibanaPageTemplate; + }, [withoutPageTemplateWrapper]); + // ------------ // Render // ------------ @@ -861,10 +891,6 @@ function TableListViewComp({ return null; } - const PageTemplate = withoutPageTemplateWrapper - ? (React.Fragment as unknown as typeof KibanaPageTemplate) - : KibanaPageTemplate; - if (!showFetchError && hasNoItems) { return ( @@ -929,6 +955,7 @@ function TableListViewComp({ tagsToTableItemMap={tagsToTableItemMap} deleteItems={deleteItems} tableCaption={tableListTitle} + tableItemsRowActions={tableItemsRowActions} onTableChange={onTableChange} onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index 0e716e6d59cf3..c8e734a289451 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -12,3 +12,16 @@ export interface Tag { description: string; color: string; } + +export type TableRowAction = 'delete'; + +export type RowActions = { + [action in TableRowAction]?: { + enabled: boolean; + reason?: string; + }; +}; + +export interface TableItemsRowActions { + [id: string]: RowActions | undefined; +} diff --git a/packages/shared-ux/file/types/base_file_client.ts b/packages/shared-ux/file/types/base_file_client.ts index 4a00f2de00516..52d1ca09fd170 100644 --- a/packages/shared-ux/file/types/base_file_client.ts +++ b/packages/shared-ux/file/types/base_file_client.ts @@ -27,6 +27,7 @@ export interface BaseFilesClient { find: ( args: { kind?: string | string[]; + kindToExclude?: string | string[]; status?: string | string[]; extension?: string | string[]; name?: string | string[]; diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 4c49124f7149f..86b9e47fdab43 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -250,6 +250,22 @@ export interface FileKindBrowser extends FileKindBase { * @default 4MiB */ maxSizeBytes?: number; + /** + * Allowed actions that can be done in the File Management UI. If not provided, all actions are allowed + * + */ + managementUiActions?: { + /** Allow files to be listed in management UI */ + list?: { + enabled: boolean; + }; + /** Allow files to be deleted in management UI */ + delete?: { + enabled: boolean; + /** If delete is not enabled in management UI, specify the reason (will appear in a tooltip). */ + reason?: string; + }; + }; } /** diff --git a/src/plugins/files/public/plugin.ts b/src/plugins/files/public/plugin.ts index 54646e9199f9a..13828d0ee366c 100644 --- a/src/plugins/files/public/plugin.ts +++ b/src/plugins/files/public/plugin.ts @@ -35,7 +35,10 @@ export interface FilesSetup { registerFileKind(fileKind: FileKindBrowser): void; } -export type FilesStart = Pick; +export type FilesStart = Pick & { + getFileKindDefinition: (id: string) => FileKindBrowser; + getAllFindKindDefinitions: () => FileKindBrowser[]; +}; /** * Bringing files to Kibana @@ -77,6 +80,12 @@ export class FilesPlugin implements Plugin { start(core: CoreStart): FilesStart { return { filesClientFactory: this.filesClientFactory!, + getFileKindDefinition: (id: string): FileKindBrowser => { + return this.registry.get(id); + }, + getAllFindKindDefinitions: (): FileKindBrowser[] => { + return this.registry.getAll(); + }, }; } } diff --git a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts index 0f453a1b81e6a..014e57b41d2b1 100644 --- a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts +++ b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts @@ -24,6 +24,7 @@ export function filterArgsToKuery({ extension, mimeType, kind, + kindToExclude, meta, name, status, @@ -50,12 +51,27 @@ export function filterArgsToKuery({ } }; + const addExcludeFilters = (fieldName: keyof FileMetadata | string, values: string[] = []) => { + if (values.length) { + const andExpressions = values + .filter(Boolean) + .map((value) => + nodeTypes.function.buildNode( + 'not', + nodeBuilder.is(`${attrPrefix}.${fieldName}`, escapeKuery(value)) + ) + ); + kueryExpressions.push(nodeBuilder.and(andExpressions)); + } + }; + addFilters('name', name, true); addFilters('FileKind', kind); addFilters('Status', status); addFilters('extension', extension); addFilters('mime_type', mimeType); addFilters('user.id', user); + addExcludeFilters('FileKind', kindToExclude); if (meta) { const addMetaFilters = pipe( diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index 4247f567802ed..96795ac93b387 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -82,6 +82,10 @@ export interface FindFileArgs extends Pagination { * File kind(s), see {@link FileKind}. */ kind?: string[]; + /** + * File kind(s) to exclude from search, see {@link FileKind}. + */ + kindToExclude?: string[]; /** * File name(s). */ diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index 25d7f463de03a..3492eb8e5f12c 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -157,26 +157,39 @@ describe('FileService', () => { createDisposableFile({ fileKind, name: 'foo-2' }), createDisposableFile({ fileKind, name: 'foo-3' }), createDisposableFile({ fileKind, name: 'test-3' }), + createDisposableFile({ fileKind: fileKindNonDefault, name: 'foo-1' }), ]); { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 1, }); expect(files.length).toBe(2); - expect(total).toBe(3); + expect(total).toBe(4); } { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 2, }); - expect(files.length).toBe(1); + expect(files.length).toBe(2); + expect(total).toBe(4); + } + + // Filter out fileKind + { + const { files, total } = await fileService.find({ + kindToExclude: [fileKindNonDefault], + name: ['foo*'], + perPage: 10, + page: 1, + }); + expect(files.length).toBe(3); // foo-1 from fileKindNonDefault not returned expect(total).toBe(3); } }); diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index a81a9d2ea5220..6749e06254100 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -30,6 +30,7 @@ export function toArrayOrUndefined(val?: string | string[]): undefined | string[ const rt = { body: schema.object({ kind: schema.maybe(stringOrArrayOfStrings), + kindToExclude: schema.maybe(stringOrArrayOfStrings), status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), @@ -50,12 +51,13 @@ export type Endpoint = CreateRouteDefinition< const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; const { - body: { meta, extension, kind, name, status }, + body: { meta, extension, kind, name, status, kindToExclude }, query, } = req; const { files: results, total } = await fileService.asCurrentUser().find({ kind: toArrayOrUndefined(kind), + kindToExclude: toArrayOrUndefined(kindToExclude), name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index becdd05fa0e2c..3ee4e5f52720c 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -12,20 +12,33 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; + import { useFilesManagementContext } from './context'; import { i18nTexts } from './i18n_texts'; import { EmptyPrompt, DiagnosticsFlyout, FileFlyout } from './components'; -type FilesUserContentSchema = UserContentCommonSchema; +type FilesUserContentSchema = Omit & { + attributes: { + title: string; + description?: string; + fileKind: string; + }; +}; function naivelyFuzzify(query: string): string { return query.includes('*') ? query : `*${query}*`; } export const App: FunctionComponent = () => { - const { filesClient } = useFilesManagementContext(); + const { filesClient, getFileKindDefinition, getAllFindKindDefinitions } = + useFilesManagementContext(); const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); + + const kindToExcludeFromSearch = getAllFindKindDefinitions() + .filter(({ managementUiActions }) => managementUiActions?.list?.enabled === false) + .map(({ id }) => id); + return (
@@ -37,7 +50,10 @@ export const App: FunctionComponent = () => { entityNamePlural={i18nTexts.entityNamePlural} findItems={(searchQuery) => filesClient - .find({ name: searchQuery ? naivelyFuzzify(searchQuery) : undefined }) + .find({ + name: searchQuery ? naivelyFuzzify(searchQuery) : undefined, + kindToExclude: kindToExcludeFromSearch, + }) .then(({ files, total }) => ({ hits: files.map((file) => ({ id: file.id, @@ -71,6 +87,12 @@ export const App: FunctionComponent = () => { {i18nTexts.diagnosticsFlyoutTitle} , ]} + rowItemActions={({ attributes }) => { + const definition = getFileKindDefinition(attributes.fileKind); + return { + delete: definition?.managementUiActions?.delete, + }; + }} /> {showDiagnosticsFlyout && ( setShowDiagnosticsFlyout(false)} /> diff --git a/src/plugins/files_management/public/context.tsx b/src/plugins/files_management/public/context.tsx index 18f031b84e5c1..0688c5a7edecb 100644 --- a/src/plugins/files_management/public/context.tsx +++ b/src/plugins/files_management/public/context.tsx @@ -12,9 +12,16 @@ import type { AppContext } from './types'; const FilesManagementAppContext = createContext(null as unknown as AppContext); -export const FilesManagementAppContextProvider: FC = ({ children, filesClient }) => { +export const FilesManagementAppContextProvider: FC = ({ + children, + filesClient, + getFileKindDefinition, + getAllFindKindDefinitions, +}) => { return ( - + {children} ); diff --git a/src/plugins/files_management/public/i18n_texts.ts b/src/plugins/files_management/public/i18n_texts.ts index c5f4956af372f..d430038dcdddc 100644 --- a/src/plugins/files_management/public/i18n_texts.ts +++ b/src/plugins/files_management/public/i18n_texts.ts @@ -101,4 +101,7 @@ export const i18nTexts = { defaultMessage: 'Upload error', }), } as Record, + rowCheckboxDisabled: i18n.translate('filesManagement.table.checkBoxDisabledLabel', { + defaultMessage: 'This file cannot be deleted.', + }), }; diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 7dce1986237a7..9c7091516d46e 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -30,6 +30,10 @@ export const mountManagementSection = ( startDeps: StartDependencies, { element, history }: ManagementAppMountParams ) => { + const { + files: { filesClientFactory, getAllFindKindDefinitions, getFileKindDefinition }, + } = startDeps; + ReactDOM.render( @@ -41,7 +45,9 @@ export const mountManagementSection = ( }} > diff --git a/src/plugins/files_management/public/types.ts b/src/plugins/files_management/public/types.ts index 2a73b69bea017..303d5e1c5d1a7 100755 --- a/src/plugins/files_management/public/types.ts +++ b/src/plugins/files_management/public/types.ts @@ -11,6 +11,8 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; export interface AppContext { filesClient: FilesClient; + getFileKindDefinition: FilesStart['getFileKindDefinition']; + getAllFindKindDefinitions: FilesStart['getAllFindKindDefinitions']; } export interface SetupDependencies { From e951205f7e488ce0d2f53bae504c7f014a3bece3 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 24 Apr 2023 12:00:32 -0400 Subject: [PATCH 16/36] [Security Solution][Endpoint] Define possible `execute` response failure codes for use in the UI (#155316) ## Summary - Updates the list of known Response Actions response codes with codes for the `execute` response action --- .../endpoint_action_generator.ts | 65 ++++++------- .../common/endpoint/types/actions.ts | 1 + .../integration_tests/execute_action.test.tsx | 34 +++++++ .../lib/endpoint_action_response_codes.ts | 91 +++++++++++++++++++ .../response_actions_log.test.tsx | 31 +++---- 5 files changed, 173 insertions(+), 49 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 855dd3a3fe439..fdf75da0a134e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -155,38 +155,37 @@ export class EndpointActionGenerator extends BaseDataGenerator { TOutputType extends object = object, TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes >( - overrides: Partial> = {} + overrides: DeepPartial> = {} ): ActionDetails { - const details: ActionDetails = merge( - { - agents: ['agent-a'], - command: 'isolate', - completedAt: '2022-04-30T16:08:47.449Z', - hosts: { 'agent-a': { name: 'Host-agent-a' } }, - id: '123', - isCompleted: true, - isExpired: false, - wasSuccessful: true, - errors: undefined, - startedAt: '2022-04-27T16:08:47.449Z', - status: 'successful', - comment: 'thisisacomment', - createdBy: 'auserid', - parameters: undefined, - outputs: {}, - agentState: { - 'agent-a': { - errors: undefined, - isCompleted: true, - completedAt: '2022-04-30T16:08:47.449Z', - wasSuccessful: true, - }, + const details: ActionDetails = { + agents: ['agent-a'], + command: 'isolate', + completedAt: '2022-04-30T16:08:47.449Z', + hosts: { 'agent-a': { name: 'Host-agent-a' } }, + id: '123', + isCompleted: true, + isExpired: false, + wasSuccessful: true, + errors: undefined, + startedAt: '2022-04-27T16:08:47.449Z', + status: 'successful', + comment: 'thisisacomment', + createdBy: 'auserid', + parameters: undefined, + outputs: {}, + agentState: { + 'agent-a': { + errors: undefined, + isCompleted: true, + completedAt: '2022-04-30T16:08:47.449Z', + wasSuccessful: true, }, }, - overrides - ); + }; - if (details.command === 'get-file') { + const command = overrides.command ?? details.command; + + if (command === 'get-file') { if (!details.parameters) { ( details as ActionDetails< @@ -213,7 +212,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } - if (details.command === 'execute') { + if (command === 'execute') { if (!details.parameters) { ( details as ActionDetails< @@ -233,14 +232,17 @@ export class EndpointActionGenerator extends BaseDataGenerator { [details.agents[0]]: this.generateExecuteActionResponseOutput({ content: { output_file_id: getFileDownloadId(details, details.agents[0]), - ...overrides.outputs?.[details.agents[0]].content, + ...(overrides.outputs?.[details.agents[0]]?.content ?? {}), }, }), }; } } - return details as unknown as ActionDetails; + return merge(details, overrides as ActionDetails) as unknown as ActionDetails< + TOutputType, + TParameters + >; } randomGetFileFailureCode(): string { @@ -310,6 +312,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { { type: 'json', content: { + code: 'ra_execute_success_done', stdout: this.randomChoice([ this.randomString(1280), this.randomString(3580), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index d9082f8aa1d8c..f8f5da28943b8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -65,6 +65,7 @@ export interface ResponseActionGetFileOutputContent { } export interface ResponseActionExecuteOutputContent { + code: string; /* The truncated 'tail' output of the command */ stdout: string; /* The truncated 'tail' of any errors generated by the command */ diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx index c3f94871c8f67..8d5d8907924f6 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx @@ -23,6 +23,8 @@ import { getEndpointAuthzInitialStateMock } from '../../../../../../common/endpo import type { EndpointPrivileges } from '../../../../../../common/endpoint/types'; import { INSUFFICIENT_PRIVILEGES_FOR_COMMAND } from '../../../../../common/translations'; import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser'; +import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes'; +import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; jest.mock('../../../../../common/components/user_privileges'); jest.mock('../../../../../common/experimental_features_service'); @@ -180,4 +182,36 @@ describe('When using execute action from response actions console', () => { ); }); }); + + it.each( + Object.keys(endpointActionResponseCodes).filter((key) => key.startsWith('ra_execute_error')) + )('should display known error message for response failure: %s', async (errorCode) => { + apiMocks.responseProvider.actionDetails.mockReturnValue({ + data: new EndpointActionGenerator('seed').generateActionDetails({ + command: 'execute', + errors: ['some error happen in endpoint'], + wasSuccessful: false, + outputs: { + 'agent-a': { + content: { + code: errorCode, + }, + }, + }, + }), + }); + + const { getByTestId } = await render(); + enterConsoleCommand(renderResult, 'execute --command="ls -l"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(getByTestId('execute-actionFailure')).toHaveTextContent( + endpointActionResponseCodes[errorCode] + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts index 746b9201cd200..c2595c625b7fa 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts @@ -123,6 +123,97 @@ const CODES = Object.freeze({ 'xpack.securitySolution.endpointActionResponseCodes.killProcess.notPermittedSuccess', { defaultMessage: 'The provided process cannot be killed' } ), + + // ----------------------------------------------------------------- + // EXECUTE CODES + // ----------------------------------------------------------------- + + // Dev: + // Something interrupted preparing the zip: file read error, zip error. I think these should be rare, + // and should succeed on retry by the user or result in file-not-found. We might implement some retries + // internally but I'm leaning to the opinion that we should rather quickly send the feedback to the + // user to let them decide. + ra_execute_error_processing: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingError', + { + defaultMessage: 'Unable to create execution output zip file.', + } + ), + + // Dev: + // Executing timeout has been reached, the command was killed. + 'ra_execute_error_processing-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingTimeout', + { defaultMessage: 'Command execution was terminated. It exceeded the provided timeout.' } + ), + + // Dev: + // Execution was interrupted, for example: system shutdown, endpoint service stop/restart. + 'ra_execute_error_processing-interrupted': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingInterrupted', + { + defaultMessage: 'Command execution was absolutely interrupted.', + } + ), + + // Dev: + // Too many active execute actions, limit 10. Execute actions are allowed to run in parallel, we must + // take into account resource use impact on endpoint as customers are piky about CPU/MEM utilization. + 'ra_execute_error_to-many-requests': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.toManyRequests', + { + defaultMessage: 'Too many concurrent command execution actions.', + } + ), + + // Dev: + // generic failure (rare corner case, software bug, etc) + ra_execute_error_failure: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.failure', + { defaultMessage: 'Unknown failure while executing command.' } + ), + + // Dev: + // Max pending response zip uploads has been reached, limit 10. Endpoint can't use unlimited disk space. + 'ra_execute_error_disk-quota': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.diskQuotaError', + { + defaultMessage: 'Too many pending command execution output zip files.', + } + ), + + // Dev: + // The fleet upload API was unreachable (not just busy). This may mean policy misconfiguration, in which + // case health status in Kibana should indicate degraded, or maybe network configuration problems, or fleet + // server problems HTTP 500. This excludes offline status, where endpoint should just wait for network connection. + 'ra_execute_error_upload-api-unreachable': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.uploadApiUnreachable', + { + defaultMessage: + 'Failed to upload command execution output zip file. Unable to reach Fleet Server upload API.', + } + ), + + // Dev: + // Perhaps internet connection was too slow or unstable to upload all chunks before unique + // upload-id expired. Endpoint will re-try a bit, max 3 times. + 'ra_execute_error_upload-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.outputUploadTimeout', + { + defaultMessage: 'Failed to upload command execution output zip file. Upload timed out', + } + ), + + // DEV: + // Upload API could be busy, endpoint should periodically re-try (2 days = 192 x 15min, assuming + // that with 1Mbps 15min is enough to upload 100MB) + 'ra_execute_error_queue-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.queueTimeout', + { + defaultMessage: + 'Failed to upload command execution output zip file. Timed out while queued waiting for Fleet Server', + } + ), }); /** diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 6360b55b1c06f..6fc62dd6cf851 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -652,10 +652,14 @@ describe('Response actions history', () => { }); it('should contain expected output accordions for `execute` action WITH execute operation privilege', async () => { - const actionDetails = await getActionListMock({ actionCount: 1, commands: ['execute'] }); + const actionListApiResponse = await getActionListMock({ + actionCount: 1, + agentIds: ['agent-a'], + commands: ['execute'], + }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: actionDetails, + data: actionListApiResponse, }); mockUseGetFileInfo = { @@ -669,17 +673,7 @@ describe('Response actions history', () => { isFetched: true, error: null, data: { - data: { - ...apiMocks.responseProvider.actionDetails({ - path: `/api/endpoint/action/${actionDetails.data[0].id}`, - }).data, - outputs: { - [actionDetails.data[0].agents[0]]: { - content: {}, - type: 'json', - }, - }, - }, + data: actionListApiResponse.data[0], }, }; @@ -714,7 +708,11 @@ describe('Response actions history', () => { }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: await getActionListMock({ actionCount: 1, commands: ['execute'] }), + data: await getActionListMock({ + actionCount: 1, + commands: ['execute'], + agentIds: ['agent-a'], + }), }); render(); @@ -723,10 +721,7 @@ describe('Response actions history', () => { const expandButton = getByTestId(`${testPrefix}-expand-button`); userEvent.click(expandButton); - const executeAccordions = getByTestId( - `${testPrefix}-actionsLogTray-executeResponseOutput-output` - ); - expect(executeAccordions).toBeTruthy(); + expect(getByTestId(`${testPrefix}-actionsLogTray-executeResponseOutput-output`)); }); it('should not contain full output download link in expanded row for `execute` action WITHOUT Actions Log privileges', async () => { From 50e3ff2f23ea38396c58961d4282674b9e2da0dd Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 24 Apr 2023 18:17:23 +0200 Subject: [PATCH 17/36] [Synthetics] Add license issued_to value in request (#155600) --- .../service_api_client.test.ts | 40 +++++++++++++++---- .../synthetics_service/service_api_client.ts | 12 +++--- .../synthetics_service/synthetics_service.ts | 12 +++--- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts index 22ec8e8a3213e..2ed86eb91ae52 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts @@ -14,6 +14,24 @@ import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { ServiceConfig } from '../../common/config'; import axios from 'axios'; import { LocationStatus, PublicLocations } from '../../common/runtime_types'; +import { LicenseGetResponse } from '@elastic/elasticsearch/lib/api/types'; + +const licenseMock: LicenseGetResponse = { + license: { + status: 'active', + uid: '1d34eb9f-e66f-47d1-8d24-cd60d187587a', + type: 'trial', + issue_date: '2022-05-05T14:25:00.732Z', + issue_date_in_millis: 165176070074432, + expiry_date: '2022-06-04T14:25:00.732Z', + expiry_date_in_millis: 165435270073332, + max_nodes: 1000, + max_resource_units: null, + issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', + issuer: 'elasticsearch', + start_date_in_millis: -1, + }, +}; jest.mock('axios', () => jest.fn()); jest.mock('@kbn/server-http-tools', () => ({ @@ -167,7 +185,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(spy).toHaveBeenCalledTimes(3); @@ -181,7 +199,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', devUrl @@ -195,7 +213,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_qa') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.elstc.co' @@ -209,7 +227,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_staging') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.stg.co' @@ -223,6 +241,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -242,6 +261,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -261,6 +281,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -324,7 +345,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'platinum', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -333,7 +354,8 @@ describe('callAPI', () => { is_edit: undefined, output, stack_version: '8.7.0', - license_level: 'platinum', + license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -376,7 +398,7 @@ describe('callAPI', () => { await apiClient.runOnce({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -386,6 +408,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -428,7 +451,7 @@ describe('callAPI', () => { await apiClient.syncMonitors({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -438,6 +461,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index bfc872f3680a8..4fea1d04b64e4 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -11,6 +11,7 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '@kbn/core/server'; +import { LicenseGetLicenseInformation } from '@elastic/elasticsearch/lib/api/types'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { MonitorFields, PublicLocations, ServiceLocationErrors } from '../../common/runtime_types'; @@ -27,7 +28,7 @@ export interface ServiceData { }; endpoint?: 'monitors' | 'runOnce' | 'sync'; isEdit?: boolean; - licenseLevel: string; + license: LicenseGetLicenseInformation; } export class ServiceAPIClient { @@ -142,7 +143,7 @@ export class ServiceAPIClient { async callAPI( method: 'POST' | 'PUT' | 'DELETE', - { monitors: allMonitors, output, endpoint, isEdit, licenseLevel }: ServiceData + { monitors: allMonitors, output, endpoint, isEdit, license }: ServiceData ) { if (this.username === TEST_SERVICE_USERNAME) { // we don't want to call service while local integration tests are running @@ -159,7 +160,7 @@ export class ServiceAPIClient { ); if (locMonitors.length > 0) { const promise = this.callServiceEndpoint( - { monitors: locMonitors, isEdit, endpoint, output, licenseLevel }, + { monitors: locMonitors, isEdit, endpoint, output, license }, method, url ); @@ -200,7 +201,7 @@ export class ServiceAPIClient { } async callServiceEndpoint( - { monitors, output, endpoint = 'monitors', isEdit, licenseLevel }: ServiceData, + { monitors, output, endpoint = 'monitors', isEdit, license }: ServiceData, method: 'POST' | 'PUT' | 'DELETE', baseUrl: string ) { @@ -233,7 +234,8 @@ export class ServiceAPIClient { output, stack_version: this.stackVersion, is_edit: isEdit, - license_level: licenseLevel, + license_level: license.type, + license_issued_to: license.issued_to, }, headers: authHeader, httpsAgent: this.getHttpsAgent(baseUrl), diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index bd9c0032984c3..e4ef7cbbb6e67 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -307,7 +307,7 @@ export class SyntheticsService { this.syncErrors = await this.apiClient.post({ monitors, output, - licenseLevel: license.type, + license, }); } return this.syncErrors; @@ -329,7 +329,7 @@ export class SyntheticsService { monitors, output, isEdit, - licenseLevel: license.type, + license, }; this.syncErrors = await this.apiClient.put(data); @@ -372,7 +372,7 @@ export class SyntheticsService { service.syncErrors = await this.apiClient.syncMonitors({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { sendErrorTelemetryEvents(service.logger, service.server.telemetry, { @@ -406,7 +406,7 @@ export class SyntheticsService { return await this.apiClient.runOnce({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { this.logger.error(e); @@ -429,7 +429,7 @@ export class SyntheticsService { const data = { output, monitors: this.formatConfigs(configs), - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } @@ -453,7 +453,7 @@ export class SyntheticsService { const data = { output, monitors, - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } From 3e94b43aa28f12538547217a0eeec224e7fa4263 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Mon, 24 Apr 2023 18:47:44 +0200 Subject: [PATCH 18/36] [ML] AIOps: Link from Explain Log Rate Spikes to Log Pattern Analysis (#155121) Adds table actions to Explain Log Rate Spikes to be able to drill down to Log Pattern Analysis. --- .../public/application/utils/url_state.ts | 45 +++++ .../explain_log_rate_spikes_app_state.tsx | 30 ---- .../explain_log_rate_spikes_page.tsx | 10 +- .../category_table/category_table.tsx | 4 +- .../log_categorization_page.tsx | 22 ++- .../log_categorization/use_discover_links.ts | 2 +- .../spike_analysis_table.tsx | 4 +- .../spike_analysis_table_groups.tsx | 4 +- .../table_action_button.tsx | 62 +++++++ .../use_copy_to_clipboard_action.test.tsx | 26 ++- .../use_copy_to_clipboard_action.tsx | 20 ++- .../use_view_in_discover_action.tsx | 36 ++-- ...se_view_in_log_pattern_analysis_action.tsx | 108 ++++++++++++ x-pack/plugins/aiops/public/hooks/use_data.ts | 2 +- x-pack/plugins/aiops/tsconfig.json | 1 + .../apps/aiops/explain_log_rate_spikes.ts | 158 +++++++++++------- .../test/functional/apps/aiops/test_data.ts | 106 ++++++++++++ x-pack/test/functional/apps/aiops/types.ts | 12 +- ...n_log_rate_spikes_analysis_groups_table.ts | 50 ++++++ .../explain_log_rate_spikes_data_generator.ts | 8 + .../aiops/explain_log_rate_spikes_page.ts | 8 +- .../test/functional/services/aiops/index.ts | 3 + .../aiops/log_pattern_analysis_page.ts | 42 +++++ 23 files changed, 631 insertions(+), 132 deletions(-) create mode 100644 x-pack/plugins/aiops/public/application/utils/url_state.ts create mode 100644 x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx create mode 100644 x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx create mode 100644 x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts diff --git a/x-pack/plugins/aiops/public/application/utils/url_state.ts b/x-pack/plugins/aiops/public/application/utils/url_state.ts new file mode 100644 index 0000000000000..9fdaa443f4c75 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/utils/url_state.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { Filter, Query } from '@kbn/es-query'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from './search_utils'; + +const defaultSearchQuery = { + match_all: {}, +}; + +export interface AiOpsPageUrlState { + pageKey: 'AIOPS_INDEX_VIEWER'; + pageUrlState: AiOpsIndexBasedAppState; +} + +export interface AiOpsIndexBasedAppState { + searchString?: Query['query']; + searchQuery?: estypes.QueryDslQueryContainer; + searchQueryLanguage: SearchQueryLanguage; + filters?: Filter[]; +} + +export type AiOpsFullIndexBasedAppState = Required; + +export const getDefaultAiOpsListState = ( + overrides?: Partial +): AiOpsFullIndexBasedAppState => ({ + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + filters: [], + ...overrides, +}); + +export const isFullAiOpsListState = (arg: unknown): arg is AiOpsFullIndexBasedAppState => { + return isPopulatedObject(arg, Object.keys(getDefaultAiOpsListState())); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx index ce69ea9fea3ae..b30e3a7d1e6fb 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx @@ -10,7 +10,6 @@ import { pick } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; -import type { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -21,7 +20,6 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; -import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; import { AiopsAppContext } from '../../hooks/use_aiops_app_context'; import { DataSourceContext } from '../../hooks/use_data_source'; @@ -42,34 +40,6 @@ export interface ExplainLogRateSpikesAppStateProps { appDependencies: AiopsAppDependencies; } -const defaultSearchQuery = { - match_all: {}, -}; - -export interface AiOpsPageUrlState { - pageKey: 'AIOPS_INDEX_VIEWER'; - pageUrlState: AiOpsIndexBasedAppState; -} - -export interface AiOpsIndexBasedAppState { - searchString?: Query['query']; - searchQuery?: Query['query']; - searchQueryLanguage: SearchQueryLanguage; - filters?: Filter[]; -} - -export const getDefaultAiOpsListState = ( - overrides?: Partial -): Required => ({ - searchString: '', - searchQuery: defaultSearchQuery, - searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, - filters: [], - ...overrides, -}); - -export const restorableDefaults = getDefaultAiOpsListState(); - export const ExplainLogRateSpikesAppState: FC = ({ dataView, savedSearch, diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index 80640f59901bc..fb9ce01c63391 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useState, FC } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiEmptyPrompt, EuiFlexGroup, @@ -28,6 +29,10 @@ import { useDataSource } from '../../hooks/use_data_source'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; +import { + getDefaultAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; import { DocumentCountContent } from '../document_count_content/document_count_content'; import { SearchPanel } from '../search_panel'; @@ -35,7 +40,6 @@ import type { GroupTableItem } from '../spike_analysis_table/types'; import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider'; import { PageHeader } from '../page_header'; -import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state'; import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis'; function getDocumentCountStatsSplitLabel( @@ -66,7 +70,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const [aiopsListState, setAiopsListState] = usePageUrlState( 'AIOPS_INDEX_VIEWER', - restorableDefaults + getDefaultAiOpsListState() ); const [globalState, setGlobalState] = useUrlState('_g'); @@ -80,7 +84,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx index c888694a7b0c3..747f90d542354 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx @@ -25,7 +25,7 @@ import { import { useDiscoverLinks } from '../use_discover_links'; import { MiniHistogram } from '../../mini_histogram'; import { useEuiTheme } from '../../../hooks/use_eui_theme'; -import type { AiOpsIndexBasedAppState } from '../../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsFullIndexBasedAppState } from '../../../application/utils/url_state'; import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request'; import { useTableState } from './use_table_state'; @@ -42,7 +42,7 @@ interface Props { dataViewId: string; selectedField: string | undefined; timefilter: TimefilterContract; - aiopsListState: Required; + aiopsListState: AiOpsFullIndexBasedAppState; pinnedCategory: Category | null; setPinnedCategory: (category: Category | null) => void; selectedCategory: Category | null; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index b673d8a498a21..7c7d0001aea42 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -4,8 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButton, EuiSpacer, @@ -21,14 +23,18 @@ import { import { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '@kbn/ml-url-state'; +import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; import { useDataSource } from '../../hooks/use_data_source'; import { useData } from '../../hooks/use_data'; import type { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { + getDefaultAiOpsListState, + isFullAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; -import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; import { SearchPanel } from '../search_panel'; import { PageHeader } from '../page_header'; @@ -47,7 +53,10 @@ export const LogCategorizationPage: FC = () => { const { dataView, savedSearch } = useDataSource(); const { runCategorizeRequest, cancelRequest } = useCategorizeRequest(); - const [aiopsListState, setAiopsListState] = useState(restorableDefaults); + const [aiopsListState, setAiopsListState] = usePageUrlState( + 'AIOPS_INDEX_VIEWER', + getDefaultAiOpsListState() + ); const [globalState, setGlobalState] = useUrlState('_g'); const [selectedField, setSelectedField] = useState(); const [selectedCategory, setSelectedCategory] = useState(null); @@ -76,7 +85,7 @@ export const LogCategorizationPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; @@ -289,7 +298,10 @@ export const LogCategorizationPage: FC = () => { fieldSelected={selectedField !== null} /> - {selectedField !== undefined && categories !== null && categories.length > 0 ? ( + {selectedField !== undefined && + categories !== null && + categories.length > 0 && + isFullAiOpsListState(aiopsListState) ? ( = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -238,7 +240,7 @@ export const SpikeAnalysisTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'middle', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index b319db0088d4d..ca55b43907bd6 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -40,6 +40,7 @@ import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_prov import type { GroupTableItem } from './types'; import { useCopyToClipboardAction } from './use_copy_to_clipboard_action'; import { useViewInDiscoverAction } from './use_view_in_discover_action'; +import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action'; const NARROW_COLUMN_WIDTH = '120px'; const EXPAND_COLUMN_WIDTH = '40px'; @@ -121,6 +122,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -355,7 +357,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'top', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx new file mode 100644 index 0000000000000..16c91f8d3851f --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx @@ -0,0 +1,62 @@ +/* + * 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 React, { type FC } from 'react'; + +import { EuiLink, EuiIcon, EuiText, EuiToolTip, type IconType } from '@elastic/eui'; + +interface TableActionButtonProps { + iconType: IconType; + dataTestSubjPostfix: string; + isDisabled: boolean; + label: string; + tooltipText?: string; + onClick: () => void; +} + +export const TableActionButton: FC = ({ + iconType, + dataTestSubjPostfix, + isDisabled, + label, + tooltipText, + onClick, +}) => { + const buttonContent = ( + <> + + {label} + + ); + + const unwrappedButton = !isDisabled ? ( + + {buttonContent} + + ) : ( + + {buttonContent} + + ); + + if (tooltipText) { + return {unwrappedButton}; + } + + return unwrappedButton; +}; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx index 82359b3d2b1aa..0984c76a4b170 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx @@ -30,11 +30,19 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a single significant term', async () => { execCommandMock.mockImplementationOnce(() => true); const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(significantTerms[0])); + const { findByText, getByTestId } = render( + (result.current as Action).render(significantTerms[0]) + ); - const button = getByLabelText('Copy field/value pair as KQL syntax to clipboard'); + const button = getByTestId('aiopsTableActionButtonCopyToClipboard enabled'); - expect(button).toBeInTheDocument(); + userEvent.hover(button); + + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect( + await findByText('Copy field/value pair as KQL syntax to clipboard') + ).toBeInTheDocument(); await act(async () => { await userEvent.click(button); @@ -50,12 +58,16 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a group of items', async () => { execCommandMock.mockImplementationOnce(() => true); const groupTableItems = getGroupTableItems(finalSignificantTermGroups); - const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(groupTableItems[0])); + const { result } = renderHook(useCopyToClipboardAction); + const { findByText, getByText } = render((result.current as Action).render(groupTableItems[0])); + + const button = getByText('Copy to clipboard'); - const button = getByLabelText('Copy group items as KQL syntax to clipboard'); + userEvent.hover(button); - expect(button).toBeInTheDocument(); + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect(await findByText('Copy group items as KQL syntax to clipboard')).toBeInTheDocument(); await act(async () => { await userEvent.click(button); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx index e9924307c1e27..1b906eb56e988 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx @@ -7,14 +7,22 @@ import React from 'react'; -import { EuiCopy, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { EuiCopy, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isSignificantTerm, type SignificantTerm } from '@kbn/ml-agg-utils'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; +const copyToClipboardButtonLabel = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardButtonLabel', + { + defaultMessage: 'Copy to clipboard', + } +); + const copyToClipboardSignificantTermMessage = i18n.translate( 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardSignificantTermMessage', { @@ -37,7 +45,15 @@ export const useCopyToClipboardAction = (): TableItemAction => ({ return ( - {(copy) => } + {(copy) => ( + + )} ); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx index 5f30abb2f6cec..bd7741bb452bf 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx @@ -7,14 +7,13 @@ import React, { useMemo } from 'react'; -import { EuiIcon, EuiToolTip } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; @@ -83,19 +82,26 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction => }; return { - name: () => ( - - - - ), - description: viewInDiscoverMessage, - type: 'button', - onClick: async (tableItem) => { - const openInDiscoverUrl = await generateDiscoverUrl(tableItem); - if (typeof openInDiscoverUrl === 'string') { - await application.navigateToUrl(openInDiscoverUrl); - } + render: (tableItem: SignificantTerm | GroupTableItem) => { + const tooltipText = discoverUrlError ? discoverUrlError : viewInDiscoverMessage; + + const clickHandler = async () => { + const openInDiscoverUrl = await generateDiscoverUrl(tableItem); + if (typeof openInDiscoverUrl === 'string') { + await application.navigateToUrl(openInDiscoverUrl); + } + }; + + return ( + + ); }, - enabled: () => discoverUrlError === undefined, }; }; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx new file mode 100644 index 0000000000000..9388cf147c8ff --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx @@ -0,0 +1,108 @@ +/* + * 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 React, { useMemo } from 'react'; + +import { SerializableRecord } from '@kbn/utility-types'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import type { SignificantTerm } from '@kbn/ml-agg-utils'; + +import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; + +import { TableActionButton } from './table_action_button'; +import { getTableItemAsKQL } from './get_table_item_as_kql'; +import type { GroupTableItem, TableItemAction } from './types'; + +const viewInLogPatternAnalysisMessage = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.viewInLogPatternAnalysis', + { + defaultMessage: 'View in Log Pattern Analysis', + } +); + +export const useViewInLogPatternAnalysisAction = (dataViewId?: string): TableItemAction => { + const { application, share, data } = useAiopsAppContext(); + + const mlLocator = useMemo(() => share.url.locators.get('ML_APP_LOCATOR'), [share.url.locators]); + + const generateLogPatternAnalysisUrl = async ( + groupTableItem: GroupTableItem | SignificantTerm + ) => { + if (mlLocator !== undefined) { + const searchString = getTableItemAsKQL(groupTableItem); + const ast = fromKueryExpression(searchString); + const searchQuery = toElasticsearchQuery(ast); + + const appState = { + AIOPS_INDEX_VIEWER: { + filters: data.query.filterManager.getFilters(), + // QueryDslQueryContainer type triggers an error as being + // not working with SerializableRecord, however, it works as expected. + searchQuery: searchQuery as unknown, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + searchString: getTableItemAsKQL(groupTableItem), + }, + } as SerializableRecord; + + return await mlLocator.getUrl({ + page: 'aiops/log_categorization', + pageState: { + index: dataViewId, + timeRange: data.query.timefilter.timefilter.getTime(), + appState, + }, + }); + } + }; + + const logPatternAnalysisUrlError = useMemo(() => { + if (!mlLocator) { + return i18n.translate('xpack.aiops.spikeAnalysisTable.mlLocatorMissingErrorMessage', { + defaultMessage: 'No locator for Log Pattern Analysis detected', + }); + } + if (!dataViewId) { + return i18n.translate( + 'xpack.aiops.spikeAnalysisTable.autoGeneratedLogPatternAnalysisLinkErrorMessage', + { + defaultMessage: + 'Unable to link to Log Pattern Analysis; no data view exists for this index', + } + ); + } + }, [dataViewId, mlLocator]); + + return { + render: (tableItem: SignificantTerm | GroupTableItem) => { + const message = logPatternAnalysisUrlError + ? logPatternAnalysisUrlError + : viewInLogPatternAnalysisMessage; + + const clickHandler = async () => { + const openInLogPatternAnalysisUrl = await generateLogPatternAnalysisUrl(tableItem); + if (typeof openInLogPatternAnalysisUrl === 'string') { + await application.navigateToUrl(openInLogPatternAnalysisUrl); + } + }; + + const isDisabled = logPatternAnalysisUrlError !== undefined; + + return ( + + ); + }, + }; +}; diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index c390582ccae1a..62f4c596cc60e 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -18,7 +18,7 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; import { PLUGIN_ID } from '../../common'; import type { DocumentStatsSearchStrategyParams } from '../get_document_stats'; -import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; import type { GroupTableItem } from '../components/spike_analysis_table/types'; diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index 89c236ba25c99..6c9aafffa26d0 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -50,6 +50,7 @@ "@kbn/ml-route-utils", "@kbn/unified-field-list-plugin", "@kbn/ml-random-sampler-utils", + "@kbn/utility-types", "@kbn/ml-error-utils", ], "exclude": [ diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 54b1baae454bd..7731eea1d9d7a 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -13,8 +13,8 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; import { isTestDataExpectedWithSampleProbability, type TestData } from './types'; import { explainLogRateSpikesTestData } from './test_data'; -export default function ({ getPageObject, getService }: FtrProviderContext) { - const headerPage = getPageObject('header'); +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'console', 'header', 'home', 'security']); const elasticChart = getService('elasticChart'); const aiops = getService('aiops'); @@ -58,7 +58,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.assertSamplingProbabilityMissing(); } - await headerPage.waitUntilLoadingHasFinished(); + await PageObjects.header.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep( `${testData.suiteTitle} displays elements in the doc count panel correctly` @@ -78,77 +78,78 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.clickDocumentCountChart(testData.chartClickCoordinates); await aiops.explainLogRateSpikesPage.assertAnalysisSectionExists(); - await ml.testExecution.logTestStep('displays the no results found prompt'); - await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); + if (testData.brushDeviationTargetTimestamp) { + await ml.testExecution.logTestStep('displays the no results found prompt'); + await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); - await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); + await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); - // Get the current width of the deviation brush for later comparison. - const brushSelectionWidthBefore = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); - - // Get the px values for the timestamp we want to move the brush to. - const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushDeviationTargetTimestamp - ); - - // Adjust the right brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--e', - targetPx + intervalPx * testData.brushIntervalFactor - ); - - // Adjust the left brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--w', - targetPx - intervalPx * (testData.brushIntervalFactor - 1) - ); + // Get the current width of the deviation brush for later comparison. + const brushSelectionWidthBefore = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); - if (testData.brushBaselineTargetTimestamp) { // Get the px values for the timestamp we want to move the brush to. - const { targetPx: targetBaselinePx } = - await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushBaselineTargetTimestamp - ); + const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushDeviationTargetTimestamp + ); // Adjust the right brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--e', - targetBaselinePx + intervalPx * testData.brushIntervalFactor + targetPx + intervalPx * testData.brushIntervalFactor ); // Adjust the left brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--w', - targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + targetPx - intervalPx * (testData.brushIntervalFactor - 1) ); - } - // Get the new brush selection width for later comparison. - const brushSelectionWidthAfter = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); + if (testData.brushBaselineTargetTimestamp) { + // Get the px values for the timestamp we want to move the brush to. + const { targetPx: targetBaselinePx } = + await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushBaselineTargetTimestamp + ); + + // Adjust the right brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--e', + targetBaselinePx + intervalPx * testData.brushIntervalFactor + ); - // Assert the adjusted brush: The selection width should have changed and - // we test if the selection is smaller than two bucket intervals. - // Finally, the adjusted brush should trigger - // a warning on the "Rerun analysis" button. - expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); - expect(brushSelectionWidthAfter).not.to.be.greaterThan( - intervalPx * 2 * testData.brushIntervalFactor - ); + // Adjust the left brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--w', + targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + ); + } + + // Get the new brush selection width for later comparison. + const brushSelectionWidthAfter = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); + + // Assert the adjusted brush: The selection width should have changed and + // we test if the selection is smaller than two bucket intervals. + // Finally, the adjusted brush should trigger + // a warning on the "Rerun analysis" button. + expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); + expect(brushSelectionWidthAfter).not.to.be.greaterThan( + intervalPx * 2 * testData.brushIntervalFactor + ); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); - await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + + await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); + } - await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); await aiops.explainLogRateSpikesPage.assertProgressTitle('Progress: 100% — Done.'); // The group switch should be disabled by default @@ -178,14 +179,14 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } - // Assert the field selector that allows to costumize grouping + await ml.testExecution.logTestStep('open the field filter'); await aiops.explainLogRateSpikesPage.assertFieldFilterPopoverButtonExists(false); await aiops.explainLogRateSpikesPage.clickFieldFilterPopoverButton(true); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList( testData.expected.fieldSelectorPopover ); - // Filter fields + await ml.testExecution.logTestStep('filter fields'); await aiops.explainLogRateSpikesPage.setFieldSelectorSearch(testData.fieldSelectorSearch); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList([ testData.fieldSelectorSearch, @@ -196,6 +197,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); if (testData.fieldSelectorApplyAvailable) { + await ml.testExecution.logTestStep('regroup results'); await aiops.explainLogRateSpikesPage.clickFieldFilterApplyButton(); if (!isTestDataExpectedWithSampleProbability(testData.expected)) { @@ -206,6 +208,28 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } } + + if (testData.action !== undefined) { + await ml.testExecution.logTestStep('check all table row actions are present'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.assertRowActions( + testData.action.tableRowId + ); + + await ml.testExecution.logTestStep('click log pattern analysis action'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.clickRowAction( + testData.action.tableRowId, + testData.action.type + ); + + await ml.testExecution.logTestStep('check log pattern analysis page loaded correctly'); + await aiops.logPatternAnalysisPageProvider.assertLogCategorizationPageExists(); + await aiops.logPatternAnalysisPageProvider.assertTotalDocumentCount( + testData.action.expected.totalDocCount + ); + await aiops.logPatternAnalysisPageProvider.assertQueryInput( + testData.action.expected.queryBar + ); + } }); } @@ -223,13 +247,27 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.testResources.setKibanaTimeZoneToUTC(); - await ml.securityUI.loginAsMlPowerUser(); + if (testData.dataGenerator === 'kibana_sample_data_logs') { + await PageObjects.security.login('elastic', 'changeme', { + expectSuccess: true, + }); + + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('logs'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } else { + await ml.securityUI.loginAsMlPowerUser(); + } }); after(async () => { await elasticChart.setNewChartUiDebugFlag(false); - await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); - + if (testData.dataGenerator !== 'kibana_sample_data_logs') { + await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); + } await aiops.explainLogRateSpikesDataGenerator.removeGeneratedData(testData.dataGenerator); }); diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index b6d3293aeba81..95e21fbfc7800 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -7,6 +7,111 @@ import type { TestData } from './types'; +export const kibanaLogsDataViewTestData: TestData = { + suiteTitle: 'kibana sample data logs', + dataGenerator: 'kibana_sample_data_logs', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'kibana_sample_data_logs', + brushIntervalFactor: 1, + chartClickCoordinates: [235, 0], + fieldSelectorSearch: 'referer', + fieldSelectorApplyAvailable: true, + action: { + type: 'LogPatternAnalysis', + tableRowId: '488337254', + expected: { + queryBar: + 'clientip:30.156.16.164 AND host.keyword:elastic-elastic-elastic.org AND ip:30.156.16.163 AND response.keyword:404 AND machine.os.keyword:win xp AND geo.dest:IN AND geo.srcdest:US\\:IN', + totalDocCount: '100', + }, + }, + expected: { + totalDocCountFormatted: '14,074', + analysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* referer: http://www.elastic-elastic-elastic.com/success/timothy-l-kopra* response.keyword: 404Showing 5 out of 8 items. 8 items unique to this group.', + docCount: '100', + }, + ], + filteredAnalysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* response.keyword: 404* machine.os.keyword: win xpShowing 5 out of 7 items. 7 items unique to this group.', + docCount: '100', + }, + ], + analysisTable: [ + { + fieldName: 'clientip', + fieldValue: '30.156.16.164', + logRate: 'Chart type:bar chart', + pValue: '3.10e-13', + impact: 'High', + }, + { + fieldName: 'geo.dest', + fieldValue: 'IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'geo.srcdest', + fieldValue: 'US:IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'host.keyword', + fieldValue: 'elastic-elastic-elastic.org', + logRate: 'Chart type:bar chart', + pValue: '7.14e-9', + impact: 'High', + }, + { + fieldName: 'ip', + fieldValue: '30.156.16.163', + logRate: 'Chart type:bar chart', + pValue: '3.28e-13', + impact: 'High', + }, + { + fieldName: 'machine.os.keyword', + fieldValue: 'win xp', + logRate: 'Chart type:bar chart', + pValue: '0.0000997', + impact: 'Medium', + }, + { + fieldName: 'referer', + fieldValue: 'http://www.elastic-elastic-elastic.com/success/timothy-l-kopra', + logRate: 'Chart type:bar chart', + pValue: '4.74e-13', + impact: 'High', + }, + { + fieldName: 'response.keyword', + fieldValue: '404', + logRate: 'Chart type:bar chart', + pValue: '0.00000604', + impact: 'Medium', + }, + ], + fieldSelectorPopover: [ + 'clientip', + 'geo.dest', + 'geo.srcdest', + 'host.keyword', + 'ip', + 'machine.os.keyword', + 'referer', + 'response.keyword', + ], + }, +}; + export const farequoteDataViewTestData: TestData = { suiteTitle: 'farequote with spike', dataGenerator: 'farequote_with_spike', @@ -122,6 +227,7 @@ export const artificialLogDataViewTestData: TestData = { }; export const explainLogRateSpikesTestData: TestData[] = [ + kibanaLogsDataViewTestData, farequoteDataViewTestData, farequoteDataViewTestDataWithQuery, artificialLogDataViewTestData, diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 01733a8e1a2af..2093d4d961363 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -7,6 +7,15 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +interface TestDataTableActionLogPatternAnalysis { + type: 'LogPatternAnalysis'; + tableRowId: string; + expected: { + queryBar: string; + totalDocCount: string; + }; +} + interface TestDataExpectedWithSampleProbability { totalDocCountFormatted: string; sampleProbabilityFormatted: string; @@ -40,11 +49,12 @@ export interface TestData { sourceIndexOrSavedSearch: string; rowsPerPage?: 10 | 25 | 50; brushBaselineTargetTimestamp?: number; - brushDeviationTargetTimestamp: number; + brushDeviationTargetTimestamp?: number; brushIntervalFactor: number; chartClickCoordinates: [number, number]; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; query?: string; + action?: TestDataTableActionLogPatternAnalysis; expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts index 18cadebbf9afd..b533c50677944 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts @@ -5,12 +5,17 @@ * 2.0. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ getService, }: FtrProviderContext) { + const find = getService('find'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); return new (class AnalysisTable { public async assertSpikeAnalysisTableExists() { @@ -55,5 +60,50 @@ export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ return rows; } + + public rowSelector(rowId: string, subSelector?: string) { + const row = `~aiopsSpikeAnalysisGroupsTable > ~row-${rowId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async ensureActionsMenuOpen(rowId: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.ensureActionsMenuClosed(); + + if (!(await find.existsByCssSelector('.euiContextMenuPanel', 1000))) { + await testSubjects.click(this.rowSelector(rowId, 'euiCollapsedItemActionsButton')); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + true, + 'Actions popover should exist' + ); + } + }); + } + + public async ensureActionsMenuClosed() { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + false, + 'Actions popover should not exist' + ); + }); + } + + public async assertRowActions(rowId: string) { + await this.ensureActionsMenuOpen(rowId); + + await testSubjects.existOrFail('aiopsTableActionButtonCopyToClipboard enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonDiscover enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonLogPatternAnalysis enabled'); + + await this.ensureActionsMenuClosed(); + } + + public async clickRowAction(rowId: string, action: string) { + await this.ensureActionsMenuOpen(rowId); + await testSubjects.click(`aiopsTableActionButton${action} enabled`); + await testSubjects.missingOrFail(`aiopsTableActionButton${action} enabled`); + } })(); } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts index 1a80ac679f29b..228d47bbc746f 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts @@ -122,6 +122,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro return new (class DataGenerator { public async generateData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // will be added via UI + break; + case 'farequote_with_spike': await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); @@ -191,6 +195,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro public async removeGeneratedData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // do not remove + break; + case 'farequote_with_spike': await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); break; diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 3da9ed7c760b7..736437a1d3976 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -229,9 +229,11 @@ export function ExplainLogRateSpikesPageProvider({ }, async assertProgressTitle(expectedProgressTitle: string) { - await testSubjects.existOrFail('aiopProgressTitle'); - const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); - expect(currentProgressTitle).to.be(expectedProgressTitle); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopProgressTitle'); + const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); + expect(currentProgressTitle).to.be(expectedProgressTitle); + }); }, async navigateToIndexPatternSelection() { diff --git a/x-pack/test/functional/services/aiops/index.ts b/x-pack/test/functional/services/aiops/index.ts index 4816d37bcff04..8c208f182f3bd 100644 --- a/x-pack/test/functional/services/aiops/index.ts +++ b/x-pack/test/functional/services/aiops/index.ts @@ -11,6 +11,7 @@ import { ExplainLogRateSpikesPageProvider } from './explain_log_rate_spikes_page import { ExplainLogRateSpikesAnalysisTableProvider } from './explain_log_rate_spikes_analysis_table'; import { ExplainLogRateSpikesAnalysisGroupsTableProvider } from './explain_log_rate_spikes_analysis_groups_table'; import { ExplainLogRateSpikesDataGeneratorProvider } from './explain_log_rate_spikes_data_generator'; +import { LogPatternAnalysisPageProvider } from './log_pattern_analysis_page'; export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesPage = ExplainLogRateSpikesPageProvider(context); @@ -18,11 +19,13 @@ export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesAnalysisGroupsTable = ExplainLogRateSpikesAnalysisGroupsTableProvider(context); const explainLogRateSpikesDataGenerator = ExplainLogRateSpikesDataGeneratorProvider(context); + const logPatternAnalysisPageProvider = LogPatternAnalysisPageProvider(context); return { explainLogRateSpikesPage, explainLogRateSpikesAnalysisTable, explainLogRateSpikesAnalysisGroupsTable, explainLogRateSpikesDataGenerator, + logPatternAnalysisPageProvider, }; } diff --git a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts new file mode 100644 index 0000000000000..37872b8d7c051 --- /dev/null +++ b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts @@ -0,0 +1,42 @@ +/* + * 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 type { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogPatternAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + return { + async assertLogCategorizationPageExists() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsLogCategorizationPage'); + }); + }, + + async assertQueryInput(expectedQueryString: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + const actualQueryString = await aiopsQueryInput.getVisibleText(); + expect(actualQueryString).to.eql( + expectedQueryString, + `Expected query bar text to be '${expectedQueryString}' (got '${actualQueryString}')` + ); + }, + + async assertTotalDocumentCount(expectedFormattedTotalDocCount: string) { + await retry.tryForTime(5000, async () => { + const docCount = await testSubjects.getVisibleText('aiopsTotalDocCount'); + expect(docCount).to.eql( + expectedFormattedTotalDocCount, + `Expected total document count to be '${expectedFormattedTotalDocCount}' (got '${docCount}')` + ); + }); + }, + }; +} From 06545277d7dfc379d69c2e68879fc409fa5b71fd Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 24 Apr 2023 15:02:44 -0300 Subject: [PATCH 19/36] [Infrastructure UI] Replace Snapshot API with InfraMetrics API in Hosts View (#155531) closes [#154443](https://github.com/elastic/kibana/issues/154443) ## Summary This PR replaces the usage of the Snapshot API in favor of the new `metrics/infra` endpoint and also includes a new control in the Search Bar to allow users to select how many hosts they want the API to return. https://user-images.githubusercontent.com/2767137/233728658-bccc7258-6955-47fb-8f7b-85ef6ec5d0f9.mov Because the KPIs now needs to show an "Average (of X hosts)", they will only start fetching the data once the table has been loaded. The hosts count KPI tile was not converted to Lens, because the page needs to know the total number of hosts. ### Possible follow-up Since now everything depends on the table to be loaded, I have been experimenting with batched requests to the new API. The idea is to fetch at least the host names as soon as possible. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../http_api/infra/get_infra_metrics.ts | 6 +- .../infra/public/hooks/use_lens_attributes.ts | 8 +- .../hosts/components/chart/lens_wrapper.tsx | 23 +++- .../components/chart/metric_chart_wrapper.tsx | 70 ++-------- .../hosts/components/hosts_container.tsx | 2 +- .../hosts/components/kpis/hosts_tile.tsx | 44 ++++-- .../hosts/components/kpis/kpi_grid.tsx | 62 +++------ .../metrics/hosts/components/kpis/tile.tsx | 64 +++++++-- .../{ => search_bar}/controls_content.tsx | 22 ++- .../components/search_bar/limit_options.tsx | 81 +++++++++++ .../search_bar/unified_search_bar.tsx | 126 +++++++++++++++++ .../components/tabs/logs/logs_tab_content.tsx | 7 +- .../components/tabs/metrics/metric_chart.tsx | 4 +- .../components/tabs/metrics/metrics_grid.tsx | 36 ++--- .../hosts/components/unified_search_bar.tsx | 98 ------------- .../public/pages/metrics/hosts/constants.ts | 6 +- .../metrics/hosts/hooks/use_alerts_query.ts | 4 +- .../metrics/hosts/hooks/use_data_view.ts | 1 + .../metrics/hosts/hooks/use_host_count.ts | 129 ++++++++++++++++++ .../hosts/hooks/use_hosts_table.test.ts | 31 +++-- .../metrics/hosts/hooks/use_hosts_table.tsx | 63 +++++---- .../metrics/hosts/hooks/use_hosts_view.ts | 91 ++++++------ .../metrics/hosts/hooks/use_unified_search.ts | 30 ++-- .../hooks/use_unified_search_url_state.ts | 18 ++- .../infra/public/pages/metrics/hosts/types.ts | 4 +- .../server/routes/infra/lib/constants.ts | 3 + x-pack/plugins/infra/tsconfig.json | 2 +- .../translations/translations/fr-FR.json | 10 +- .../translations/translations/ja-JP.json | 10 +- .../translations/translations/zh-CN.json | 10 +- .../api_integration/apis/metrics_ui/infra.ts | 4 + 31 files changed, 673 insertions(+), 396 deletions(-) rename x-pack/plugins/infra/public/pages/metrics/hosts/components/{ => search_bar}/controls_content.tsx (79%) create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts diff --git a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts index bcfeeafcee06f..80e5e501169d6 100644 --- a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts +++ b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts @@ -23,8 +23,9 @@ export const RangeRT = rt.type({ }); export const InfraAssetMetadataTypeRT = rt.keyof({ - 'host.os.name': null, 'cloud.provider': null, + 'host.ip': null, + 'host.os.name': null, }); export const InfraAssetMetricsRT = rt.type({ @@ -35,7 +36,7 @@ export const InfraAssetMetricsRT = rt.type({ export const InfraAssetMetadataRT = rt.type({ // keep the actual field name from the index mappings name: InfraAssetMetadataTypeRT, - value: rt.union([rt.string, rt.number, rt.null]), + value: rt.union([rt.string, rt.null]), }); export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([ @@ -64,6 +65,7 @@ export const GetInfraMetricsResponsePayloadRT = rt.type({ export type InfraAssetMetrics = rt.TypeOf; export type InfraAssetMetadata = rt.TypeOf; +export type InfraAssetMetadataType = rt.TypeOf; export type InfraAssetMetricType = rt.TypeOf; export type InfraAssetMetricsItem = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index c9ce48c909f9a..6250d20750e29 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -74,11 +74,7 @@ export const useLensAttributes = ({ return visualizationAttributes; }, [dataView, formulaAPI, options, type, visualizationType]); - const injectFilters = (data: { - timeRange: TimeRange; - filters: Filter[]; - query: Query; - }): LensAttributes | null => { + const injectFilters = (data: { filters: Filter[]; query: Query }): LensAttributes | null => { if (!attributes) { return null; } @@ -121,7 +117,7 @@ export const useLensAttributes = ({ return true; }, async execute(_context: ActionExecutionContext): Promise { - const injectedAttributes = injectFilters({ timeRange, filters, query }); + const injectedAttributes = injectFilters({ filters, query }); if (injectedAttributes) { navigateToPrefilledEditor( { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9a2472949f54c..34e536aaf37d2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -15,7 +15,7 @@ import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; import { ChartLoader } from './chart_loader'; -export interface Props { +export interface LensWrapperProps { id: string; attributes: LensAttributes | null; dateRange: TimeRange; @@ -42,11 +42,12 @@ export const LensWrapper = ({ lastReloadRequestTime, loading = false, hasTitle = false, -}: Props) => { +}: LensWrapperProps) => { const intersectionRef = useRef(null); const [loadedOnce, setLoadedOnce] = useState(false); const [state, setState] = useState({ + attributes, lastReloadRequestTime, query, filters, @@ -65,15 +66,23 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { setState({ + attributes, lastReloadRequestTime, query, - dateRange, filters, + dateRange, }); } - }, [dateRange, filters, intersection?.intersectionRatio, lastReloadRequestTime, query]); + }, [ + attributes, + dateRange, + filters, + intersection?.intersectionRatio, + lastReloadRequestTime, + query, + ]); - const isReady = attributes && intersectedOnce; + const isReady = state.attributes && intersectedOnce; return (
@@ -83,11 +92,11 @@ export const LensWrapper = ({ style={style} hasTitle={hasTitle} > - {isReady && ( + {state.attributes && ( ; - -type AcceptedType = SnapshotMetricType | 'hostsCount'; - -export interface ChartBaseProps - extends Pick< - MetricWTrend, - 'title' | 'color' | 'extra' | 'subtitle' | 'trendA11yDescription' | 'trendA11yTitle' - > { - type: AcceptedType; - toolTip: string; - metricType: MetricType; - ['data-test-subj']?: string; -} - -interface Props extends ChartBaseProps { +export interface Props extends Pick { id: string; - nodes: SnapshotNode[]; loading: boolean; - overrideValue?: number; + value: number; + toolTip: string; + ['data-test-subj']?: string; } const MIN_HEIGHT = 150; @@ -49,23 +25,13 @@ export const MetricChartWrapper = ({ extra, id, loading, - metricType, - nodes, - overrideValue, + value, subtitle, title, toolTip, - trendA11yDescription, - trendA11yTitle, - type, ...props }: Props) => { const loadedOnce = useRef(false); - const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); - const metricsTimeseries = useMemo( - () => (metrics ?? []).find((m) => m.name === type)?.timeseries, - [metrics, type] - ); useEffect(() => { if (!loadedOnce.current && !loading) { @@ -76,29 +42,13 @@ export const MetricChartWrapper = ({ }; }, [loading]); - const metricsValue = useMemo(() => { - if (overrideValue) { - return overrideValue; - } - return (metrics ?? []).find((m) => m.name === type)?.[metricType] ?? 0; - }, [metricType, metrics, overrideValue, type]); - const metricsData: MetricWNumber = { title, subtitle, color, extra, - value: metricsValue, - valueFormatter: (d: number) => - type === 'hostsCount' ? d.toString() : createInventoryMetricFormatter({ type })(d), - ...(!!metricsTimeseries - ? { - trend: metricsTimeseries.rows.map((row) => ({ x: row.timestamp, y: row.metric_0 ?? 0 })), - trendShape: MetricTrendShape.Area, - trendA11yTitle, - trendA11yDescription, - } - : {}), + value, + valueFormatter: (d: number) => d.toString(), }; return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index 0c965feca8e9e..d42944857af34 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { UnifiedSearchBar } from './unified_search_bar'; +import { UnifiedSearchBar } from './search_bar/unified_search_bar'; import { HostsTable } from './hosts_table'; import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx index 396c0bd72ad71..14a617682bf25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx @@ -4,22 +4,46 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; -import { useHostsViewContext } from '../../hooks/use_hosts_view'; -import { type ChartBaseProps, MetricChartWrapper } from '../chart/metric_chart_wrapper'; +import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper'; -export const HostsTile = ({ type, ...props }: ChartBaseProps) => { - const { hostNodes, loading } = useHostsViewContext(); +const HOSTS_CHART: Omit = { + id: `metric-hostCount`, + color: '#6DCCB1', + title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { + defaultMessage: 'Hosts', + }), + toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { + defaultMessage: 'The number of hosts returned by your current search criteria.', + }), + ['data-test-subj']: 'hostsView-metricsTrend-hosts', +}; + +export const HostsTile = () => { + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + const { searchCriteria } = useUnifiedSearchContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit', { + defaultMessage: 'Limited to {limit}', + values: { + limit: searchCriteria.limit, + }, + }) + : undefined; + }; return ( ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 2dbd0c4324eca..c3f751d26befb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { KPIChartProps, Tile } from './tile'; +import { HostCountProvider } from '../../hooks/use_host_count'; import { HostsTile } from './hosts_tile'; -import { ChartBaseProps } from '../chart/metric_chart_wrapper'; -const KPI_CHARTS: KPIChartProps[] = [ +const KPI_CHARTS: Array> = [ { type: 'cpu', trendLine: true, @@ -20,9 +20,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.title', { defaultMessage: 'CPU usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.tooltip', { defaultMessage: 'Average of percentage of CPU time spent in states other than Idle and IOWait, normalized by the number of CPU cores. Includes both time spent on user space and kernel space. 100% means all CPUs of the host are busy.', @@ -35,9 +32,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.title', { defaultMessage: 'Memory usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.tooltip', { defaultMessage: "Average of percentage of main memory usage excluding page cache. This includes resident memory for all processes plus memory used by the kernel structures and code apart the page cache. A high level indicates a situation of memory saturation for a host. 100% means the main memory is entirely filled with memory that can't be reclaimed, except by swapping out.", @@ -50,9 +44,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.title', { defaultMessage: 'Network inbound (RX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -65,9 +56,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.title', { defaultMessage: 'Network outbound (TX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -75,38 +63,24 @@ const KPI_CHARTS: KPIChartProps[] = [ }, ]; -const HOSTS_CHART: ChartBaseProps = { - type: 'hostsCount', - color: '#6DCCB1', - metricType: 'value', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { - defaultMessage: 'Hosts', - }), - trendA11yTitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.a11y.title', { - defaultMessage: 'Hosts count.', - }), - toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { - defaultMessage: 'The number of hosts returned by your current search criteria.', - }), - ['data-test-subj']: 'hostsView-metricsTrend-hosts', -}; - export const KPIGrid = () => { return ( - - - - - {KPI_CHARTS.map(({ ...chartProp }) => ( - - + + + + - ))} - + {KPI_CHARTS.map(({ ...chartProp }) => ( + + + + ))} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index a95f18b4a10ee..89eebeefd240e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; -import { Action } from '@kbn/ui-actions-plugin/public'; +import { i18n } from '@kbn/i18n'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; import { EuiIcon, @@ -24,6 +24,9 @@ import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import { HostsLensMetricChartFormulas } from '../../../../../common/visualizations'; import { useHostsViewContext } from '../../hooks/use_hosts_view'; import { LensWrapper } from '../chart/lens_wrapper'; +import { createHostsFilter } from '../../utils'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useAfterLoadedState } from '../../hooks/use_after_loaded_state'; export interface KPIChartProps { title: string; @@ -38,7 +41,6 @@ const MIN_HEIGHT = 150; export const Tile = ({ title, - subtitle, type, backgroundColor, toolTip, @@ -46,14 +48,28 @@ export const Tile = ({ }: KPIChartProps) => { const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { requestTs, hostNodes, loading: hostsLoading } = useHostsViewContext(); + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit', { + defaultMessage: 'Average (of {limit} hosts)', + values: { + limit: searchCriteria.limit, + }, + }) + : i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average', { + defaultMessage: 'Average', + }); + }; const { attributes, getExtraActions, error } = useLensAttributes({ type, dataView, options: { title, - subtitle, + subtitle: getSubtitle(), backgroundColor, showTrendLine: trendLine, showTitle: false, @@ -61,15 +77,24 @@ export const Tile = ({ visualizationType: 'metricChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + hostNodes.map((p) => p.name), + dataView + ); + }, [hostNodes, dataView]); + + const filters = useMemo( + () => [...searchCriteria.filters, ...searchCriteria.panelFilters, ...[hostsFilterQuery]], + [hostsFilterQuery, searchCriteria.filters, searchCriteria.panelFilters] + ); + const extraActionOptions = getExtraActions({ timeRange: searchCriteria.dateRange, filters, query: searchCriteria.query, }); - const extraActions: Action[] = [extraActionOptions.openInLens]; - const handleBrushEnd = ({ range }: BrushTriggerEvent['data']) => { const [min, max] = range; onSubmit({ @@ -81,6 +106,14 @@ export const Tile = ({ }); }; + const loading = hostsLoading || !attributes || hostCountLoading; + const { afterLoadedState } = useAfterLoadedState(loading, { + attributes, + lastReloadRequestTime: requestTs, + ...searchCriteria, + filters, + }); + return ( )} @@ -134,7 +168,7 @@ export const Tile = ({ const EuiPanelStyled = styled(EuiPanel)` .echMetric { - border-radius: ${(p) => p.theme.eui.euiBorderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; pointer-events: none; } `; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx similarity index 79% rename from x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx index a3e82b9901422..e2bd7d0c74dae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx @@ -12,15 +12,16 @@ import { type ControlGroupInput, } from '@kbn/controls-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, Filter, Query, TimeRange } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; -import { Subscription } from 'rxjs'; -import { useControlPanels } from '../hooks/use_control_panels_url_state'; +import { skipWhile, Subscription } from 'rxjs'; +import { useControlPanels } from '../../hooks/use_control_panels_url_state'; interface Props { dataView: DataView | undefined; timeRange: TimeRange; filters: Filter[]; + selectedOptions: Filter[]; query: Query; onFiltersChange: (filters: Filter[]) => void; } @@ -29,6 +30,7 @@ export const ControlsContent: React.FC = ({ dataView, filters, query, + selectedOptions, timeRange, onFiltersChange, }) => { @@ -55,15 +57,21 @@ export const ControlsContent: React.FC = ({ const loadCompleteHandler = useCallback( (controlGroup: ControlGroupAPI) => { if (!controlGroup) return; - inputSubscription.current = controlGroup.onFiltersPublished$.subscribe((newFilters) => { - onFiltersChange(newFilters); - }); + inputSubscription.current = controlGroup.onFiltersPublished$ + .pipe( + skipWhile((newFilters) => + compareFilters(selectedOptions, newFilters, COMPARE_ALL_OPTIONS) + ) + ) + .subscribe((newFilters) => { + onFiltersChange(newFilters); + }); filterSubscription.current = controlGroup .getInput$() .subscribe(({ panels }) => setControlPanels(panels)); }, - [onFiltersChange, setControlPanels] + [onFiltersChange, setControlPanels, selectedOptions] ); useEffect(() => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx new file mode 100644 index 0000000000000..1d8ce4f9c9e87 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx @@ -0,0 +1,81 @@ +/* + * 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 React from 'react'; +import { + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiToolTip, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { HOST_LIMIT_OPTIONS } from '../../constants'; +import { HostLimitOptions } from '../../types'; + +interface Props { + limit: HostLimitOptions; + onChange: (limit: number) => void; +} + +export const LimitOptions = ({ limit, onChange }: Props) => { + return ( + + + + + + + + + + + + + + + onChange(value)} + /> + + + ); +}; + +const buildId = (option: number) => `hostLimit_${option}`; +const options: EuiButtonGroupOptionProps[] = HOST_LIMIT_OPTIONS.map((option) => ({ + id: buildId(option), + label: `${option}`, + value: option, + 'data-test-subj': `hostsViewLimitSelection${option}button`, +})); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx new file mode 100644 index 0000000000000..ef515cc018839 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx @@ -0,0 +1,126 @@ +/* + * 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 React, { useMemo } from 'react'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGrid, + useEuiTheme, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../../apps/metrics_app'; +import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; +import { ControlsContent } from './controls_content'; +import { useMetricsDataViewContext } from '../../hooks/use_data_view'; +import { HostsSearchPayload } from '../../hooks/use_unified_search_url_state'; +import { LimitOptions } from './limit_options'; +import { HostLimitOptions } from '../../types'; + +export const UnifiedSearchBar = () => { + const { + services: { unifiedSearch, application }, + } = useKibanaContextForPlugin(); + const { dataView } = useMetricsDataViewContext(); + const { searchCriteria, onSubmit } = useUnifiedSearchContext(); + + const { SearchBar } = unifiedSearch.ui; + + const onLimitChange = (limit: number) => { + onSubmit({ limit }); + }; + + const onPanelFiltersChange = (panelFilters: Filter[]) => { + if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { + onSubmit({ panelFilters }); + } + }; + + const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { + // This makes sure `onQueryChange` is only called when the submit button is clicked + if (isUpdate === false) { + onSubmit(payload); + } + }; + + return ( + + + + 0.5)', + })} + onQuerySubmit={handleRefresh} + showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} + showDatePicker + showFilterBar + showQueryInput + showQueryMenu + useDefaultBehaviors + /> + + + + + + + + + + + + + + + ); +}; + +const StickyContainer = (props: { children: React.ReactNode }) => { + const { euiTheme } = useEuiTheme(); + + const top = useMemo(() => { + const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); + if (!wrapper) { + return `calc(${euiTheme.size.xxxl} * 2)`; + } + + return `${wrapper.getBoundingClientRect().top}px`; + }, [euiTheme]); + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index d5cc0b0f021d7..6813dee1caa10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -9,7 +9,6 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { InfraLoadingPanel } from '../../../../../../components/loading'; -import { SnapshotNode } from '../../../../../../../common/http_api'; import { LogStream } from '../../../../../../components/log_stream'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; @@ -30,7 +29,7 @@ export const LogsTabContent = () => { ); const logsLinkToStreamQuery = useMemo(() => { - const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); + const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name)); if (filterQuery.query && hostsFilterQueryParam) { return `${filterQuery.query} and ${hostsFilterQueryParam}`; @@ -83,12 +82,12 @@ export const LogsTabContent = () => { ); }; -const createHostsFilterQueryParam = (hostNodes: SnapshotNode[]): string => { +const createHostsFilterQueryParam = (hostNodes: string[]): string => { if (!hostNodes.length) { return ''; } - const joinedHosts = hostNodes.map((p) => p.name).join(' or '); + const joinedHosts = hostNodes.join(' or '); const hostsQueryParam = `host.name:(${joinedHosts})`; return hostsQueryParam; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 28d07b94d9437..f81228957107a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -40,13 +40,13 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest, loading } = useHostsViewContext(); + const { requestTs, loading } = useHostsViewContext(); const { currentPage } = useHostsTableContext(); // prevents updates on requestTs and serchCriteria states from relaoding the chart // we want it to reload only once the table has finished loading const { afterLoadedState } = useAfterLoadedState(loading, { - lastReloadRequestTime: baseRequest.requestTs, + lastReloadRequestTime: requestTs, ...searchCriteria, }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index e307dde0d09e5..7f3dac7a3af16 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiFlexGroup, EuiText, EuiI18n } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MetricChart, MetricChartProps } from './metric_chart'; @@ -64,32 +64,12 @@ const CHARTS_IN_ORDER: Array & { fullRo export const MetricsGrid = React.memo(() => { return ( - - - - - - {DEFAULT_BREAKDOWN_SIZE}, - attribute: name, - }} - /> - - - - - - - {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( - - - - ))} - - - + + {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( + + + + ))} + ); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx deleted file mode 100644 index 168f825a9d2d1..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 React, { useMemo } from 'react'; -import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGrid, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { EuiHorizontalRule } from '@elastic/eui'; -import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../apps/metrics_app'; -import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useUnifiedSearchContext } from '../hooks/use_unified_search'; -import { ControlsContent } from './controls_content'; -import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { HostsSearchPayload } from '../hooks/use_unified_search_url_state'; - -export const UnifiedSearchBar = () => { - const { - services: { unifiedSearch, application }, - } = useKibanaContextForPlugin(); - const { dataView } = useMetricsDataViewContext(); - const { searchCriteria, onSubmit } = useUnifiedSearchContext(); - - const { SearchBar } = unifiedSearch.ui; - - const onPanelFiltersChange = (panelFilters: Filter[]) => { - if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { - onSubmit({ panelFilters }); - } - }; - - const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { - // This makes sure `onQueryChange` is only called when the submit button is clicked - if (isUpdate === false) { - onSubmit(payload); - } - }; - - return ( - - 0.5)', - })} - onQuerySubmit={handleRefresh} - showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} - showDatePicker - showFilterBar - showQueryInput - showQueryMenu - useDefaultBehaviors - /> - - - - ); -}; - -const StickyContainer = (props: { children: React.ReactNode }) => { - const { euiTheme } = useEuiTheme(); - - const top = useMemo(() => { - const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); - if (!wrapper) { - return `calc(${euiTheme.size.xxxl} * 2)`; - } - - return `${wrapper.getBoundingClientRect().top}px`; - }, [euiTheme]); - - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index b854120a86887..69cfc446d0095 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -7,13 +7,15 @@ import { i18n } from '@kbn/i18n'; import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { AlertStatusFilter } from './types'; +import { AlertStatusFilter, HostLimitOptions } from './types'; export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_HOST_LIMIT: HostLimitOptions = 100; export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; export const ALL_ALERTS: AlertStatusFilter = { @@ -55,3 +57,5 @@ export const ALERT_STATUS_QUERY = { [ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query, [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query, }; + +export const HOST_LIMIT_OPTIONS = [10, 20, 50, 100, 500] as const; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 7a895591d68c7..200bff521d86a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -9,7 +9,7 @@ import createContainer from 'constate'; import { getTime } from '@kbn/data-plugin/common'; import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils'; import { BoolQuery, buildEsQuery, Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import { useUnifiedSearchContext } from './use_unified_search'; import { HostsState } from './use_unified_search_url_state'; import { useHostsViewContext } from './use_hosts_view'; @@ -63,7 +63,7 @@ const createAlertsEsQuery = ({ status, }: { dateRange: HostsState['dateRange']; - hostNodes: SnapshotNode[]; + hostNodes: InfraAssetMetricsItem[]; status?: AlertStatus; }): AlertsEsQuery => { const alertStatusFilter = createAlertStatusFilter(status); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts index 94e3a963075be..83fedf4292937 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts @@ -72,6 +72,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => { }, [hasError, notifications, metricAlias]); return { + metricAlias, dataView, loading, hasError, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts new file mode 100644 index 0000000000000..5575c46e621f1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts @@ -0,0 +1,129 @@ +/* + * 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 * as rt from 'io-ts'; +import { ES_SEARCH_STRATEGY, IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import { useCallback, useEffect } from 'react'; +import { catchError, map, Observable, of, startWith } from 'rxjs'; +import createContainer from 'constate'; +import type { QueryDslQueryContainer, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { useDataSearch, useLatestPartialDataSearchResponse } from '../../../../utils/data_search'; +import { useMetricsDataViewContext } from './use_data_view'; +import { useUnifiedSearchContext } from './use_unified_search'; + +export const useHostCount = () => { + const { dataView, metricAlias } = useMetricsDataViewContext(); + const { buildQuery, getParsedDateRange } = useUnifiedSearchContext(); + + const { search: fetchHostCount, requests$ } = useDataSearch({ + getRequest: useCallback(() => { + const query = buildQuery(); + const dateRange = getParsedDateRange(); + + const filters: QueryDslQueryContainer = { + bool: { + ...query.bool, + filter: [ + ...query.bool.filter, + { + exists: { + field: 'host.name', + }, + }, + { + range: { + [dataView?.timeFieldName ?? '@timestamp']: { + gte: dateRange.from, + lte: dateRange.to, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }; + + return { + request: { + params: { + allow_no_indices: true, + ignore_unavailable: true, + index: metricAlias, + size: 0, + track_total_hits: false, + body: { + query: filters, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + }, + }, + options: { strategy: ES_SEARCH_STRATEGY }, + }; + }, [buildQuery, dataView, getParsedDateRange, metricAlias]), + parseResponses: normalizeDataSearchResponse, + }); + + const { isRequestRunning, isResponsePartial, latestResponseData, latestResponseErrors } = + useLatestPartialDataSearchResponse(requests$); + + useEffect(() => { + fetchHostCount(); + }, [fetchHostCount]); + + return { + errors: latestResponseErrors, + isRequestRunning, + isResponsePartial, + data: latestResponseData ?? null, + }; +}; + +export const HostCount = createContainer(useHostCount); +export const [HostCountProvider, useHostCountContext] = HostCount; + +const INITIAL_STATE = { + data: null, + errors: [], + isPartial: true, + isRunning: true, + loaded: 0, + total: undefined, +}; +const normalizeDataSearchResponse = ( + response$: Observable>>> +) => + response$.pipe( + map((response) => ({ + data: decodeOrThrow(HostCountResponseRT)(response.rawResponse.aggregations), + errors: [], + isPartial: response.isPartial ?? false, + isRunning: response.isRunning ?? false, + loaded: response.loaded, + total: response.total, + })), + startWith(INITIAL_STATE), + catchError((error) => + of({ + ...INITIAL_STATE, + errors: [error.message ?? error], + isRunning: false, + }) + ) + ); + +const HostCountResponseRT = rt.type({ + count: rt.type({ + value: rt.number, + }), +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index a921a0daeb011..5619a788b19a7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -7,7 +7,7 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import * as useUnifiedSearchHooks from './use_unified_search'; import * as useHostsViewHooks from './use_hosts_view'; @@ -22,20 +22,20 @@ const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.Mo typeof useHostsViewHooks.useHostsViewContext >; -const mockHostNode: SnapshotNode[] = [ +const mockHostNode: InfraAssetMetricsItem[] = [ { metrics: [ { name: 'rx', - avg: 252456.92916666667, + value: 252456.92916666667, }, { name: 'tx', - avg: 252758.425, + value: 252758.425, }, { name: 'memory', - avg: 0.94525, + value: 0.94525, }, { name: 'cpu', @@ -43,25 +43,28 @@ const mockHostNode: SnapshotNode[] = [ }, { name: 'memoryTotal', - avg: 34359.738368, + value: 34359.738368, }, ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], + metadata: [ + { name: 'host.os.name', value: null }, + { name: 'cloud.provider', value: 'aws' }, + ], name: 'host-0', }, { metrics: [ { name: 'rx', - avg: 95.86339715321859, + value: 95.86339715321859, }, { name: 'tx', - avg: 110.38566859563191, + value: 110.38566859563191, }, { name: 'memory', - avg: 0.5400000214576721, + value: 0.5400000214576721, }, { name: 'cpu', @@ -69,12 +72,12 @@ const mockHostNode: SnapshotNode[] = [ }, { name: 'memoryTotal', - avg: 9.194304, + value: 9.194304, }, ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, + metadata: [ + { name: 'host.os.name', value: 'macOS' }, + { name: 'host.ip', value: '243.86.94.22' }, ], name: 'host-1', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 2d2d6c9d7f8e4..7350f402c57ec 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -15,10 +15,10 @@ import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; -import type { - SnapshotNode, - SnapshotNodeMetric, - SnapshotMetricInput, +import { + InfraAssetMetadataType, + InfraAssetMetricsItem, + InfraAssetMetricType, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; @@ -29,42 +29,55 @@ import { useUnifiedSearchContext } from './use_unified_search'; * Columns and items types */ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; +type HostMetrics = Record; -type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; - -type HostMetrics = Record; - -export interface HostNodeRow extends HostMetrics { +interface HostMetadata { os?: string | null; ip?: string | null; servicesOnHost?: number | null; title: { name: string; cloudProvider?: CloudProvider | null }; - name: string; id: string; } +export type HostNodeRow = HostMetadata & + HostMetrics & { + name: string; + }; /** * Helper functions */ -const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefined | null) => { +const formatMetric = (type: InfraAssetMetricType, value: number | undefined | null) => { return value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A'; }; -const buildItemsList = (nodes: SnapshotNode[]) => { - return nodes.map(({ metrics, path, name }) => ({ - id: `${name}-${path.at(-1)?.os ?? '-'}`, - name, - os: path.at(-1)?.os ?? '-', - ip: path.at(-1)?.ip ?? '', - title: { +const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => { + return nodes.map(({ metrics, metadata, name }) => { + const metadataKeyValue = metadata.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value, + }), + {} as Record + ); + + return { name, - cloudProvider: path.at(-1)?.cloudProvider ?? null, - }, - ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric.avg ?? metric.value; - return data; - }, {} as HostMetrics), - })) as HostNodeRow[]; + id: `${name}-${metadataKeyValue['host.os.name'] ?? '-'}`, + title: { + name, + cloudProvider: (metadataKeyValue['cloud.provider'] as CloudProvider) ?? null, + }, + os: metadataKeyValue['host.os.name'] ?? '-', + ip: metadataKeyValue['host.ip'] ?? '', + ...metrics.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value ?? 0, + }), + {} as HostMetrics + ), + }; + }); }; const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts index d0df961dc7ef9..f84acf5931ea1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts @@ -12,16 +12,21 @@ * 2.0. */ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import createContainer from 'constate'; import { BoolQuery } from '@kbn/es-query'; -import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { useSourceContext } from '../../../../containers/metrics_source'; -import { useSnapshot, type UseSnapshotRequest } from '../../inventory_view/hooks/use_snaphot'; import { useUnifiedSearchContext } from './use_unified_search'; -import { StringDateRangeTimestamp } from './use_unified_search_url_state'; +import { + GetInfraMetricsRequestBodyPayload, + GetInfraMetricsResponsePayload, + InfraAssetMetricType, +} from '../../../../../common/http_api'; +import { StringDateRange } from './use_unified_search_url_state'; -const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ +const HOST_TABLE_METRICS: Array<{ type: InfraAssetMetricType }> = [ { type: 'rx' }, { type: 'tx' }, { type: 'memory' }, @@ -30,40 +35,52 @@ const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ { type: 'memoryTotal' }, ]; +const BASE_INFRA_METRICS_PATH = '/api/metrics/infra'; + export const useHostsView = () => { const { sourceId } = useSourceContext(); - const { buildQuery, getDateRangeAsTimestamp } = useUnifiedSearchContext(); + const { + services: { http }, + } = useKibanaContextForPlugin(); + const { buildQuery, getParsedDateRange, searchCriteria } = useUnifiedSearchContext(); + const abortCtrlRef = useRef(new AbortController()); const baseRequest = useMemo( () => - createSnapshotRequest({ - dateRange: getDateRangeAsTimestamp(), + createInfraMetricsRequest({ + dateRange: getParsedDateRange(), esQuery: buildQuery(), sourceId, + limit: searchCriteria.limit, }), - [buildQuery, getDateRangeAsTimestamp, sourceId] + [buildQuery, getParsedDateRange, sourceId, searchCriteria.limit] ); - // Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias. - // For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too - // if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices - const { - loading, - error, - nodes: hostNodes, - } = useSnapshot( - { - ...baseRequest, - metrics: HOST_TABLE_METRICS, + const [state, refetch] = useAsyncFn( + () => { + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + return http.post(`${BASE_INFRA_METRICS_PATH}`, { + signal: abortCtrlRef.current.signal, + body: JSON.stringify(baseRequest), + }); }, - { abortable: true } + [baseRequest, http], + { loading: true } ); + useEffect(() => { + refetch(); + }, [refetch]); + + const { value, error, loading } = state; + return { - baseRequest, + requestTs: baseRequest.requestTs, loading, error, - hostNodes, + hostNodes: value?.nodes ?? [], }; }; @@ -73,30 +90,26 @@ export const [HostsViewProvider, useHostsViewContext] = HostsView; /** * Helpers */ -const createSnapshotRequest = ({ + +const createInfraMetricsRequest = ({ esQuery, sourceId, dateRange, + limit, }: { esQuery: { bool: BoolQuery }; sourceId: string; - dateRange: StringDateRangeTimestamp; -}): UseSnapshotRequest => ({ - filterQuery: JSON.stringify(esQuery), - metrics: [], - groupBy: [], - nodeType: 'host', - sourceId, - currentTime: dateRange.to, - includeTimeseries: false, - sendRequestImmediately: true, - timerange: { - interval: '1m', + dateRange: StringDateRange; + limit: number; +}): GetInfraMetricsRequestBodyPayload & { requestTs: number } => ({ + type: 'host', + query: esQuery, + range: { from: dateRange.from, to: dateRange.to, - ignoreLookback: true, }, - // The user might want to click on the submit button without changing the filters - // This makes sure all child components will re-render. + metrics: HOST_TABLE_METRICS, + limit, + sourceId, requestTs: Date.now(), }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index 950bd9cf3c94e..e242f58054c6c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -23,14 +23,14 @@ import { } from './use_unified_search_url_state'; const buildQuerySubmittedPayload = ( - hostState: HostsState & { dateRangeTimestamp: StringDateRangeTimestamp } + hostState: HostsState & { parsedDateRange: StringDateRangeTimestamp } ) => { - const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState; + const { panelFilters, filters, parsedDateRange, query: queryObj } = hostState; return { control_filters: panelFilters.map((filter) => JSON.stringify(filter)), filters: filters.map((filter) => JSON.stringify(filter)), - interval: telemetryTimeRangeFormatter(dateRangeTimestamp.to - dateRangeTimestamp.from), + interval: telemetryTimeRangeFormatter(parsedDateRange.to - parsedDateRange.from), query: queryObj.query, }; }; @@ -41,8 +41,8 @@ const getDefaultTimestamps = () => { const now = Date.now(); return { - from: now - DEFAULT_FROM_IN_MILLISECONDS, - to: now, + from: new Date(now - DEFAULT_FROM_IN_MILLISECONDS).toISOString(), + to: new Date(now).toISOString(), }; }; @@ -63,16 +63,25 @@ export const useUnifiedSearch = () => { const onSubmit = (params?: HostsSearchPayload) => setSearch(params ?? {}); - const getDateRangeAsTimestamp = useCallback(() => { + const getParsedDateRange = useCallback(() => { const defaults = getDefaultTimestamps(); - const from = DateMath.parse(searchCriteria.dateRange.from)?.valueOf() ?? defaults.from; + const from = DateMath.parse(searchCriteria.dateRange.from)?.toISOString() ?? defaults.from; const to = - DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.valueOf() ?? defaults.to; + DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.toISOString() ?? defaults.to; return { from, to }; }, [searchCriteria.dateRange]); + const getDateRangeAsTimestamp = useCallback(() => { + const parsedDate = getParsedDateRange(); + + const from = new Date(parsedDate.from).getTime(); + const to = new Date(parsedDate.to).getTime(); + + return { from, to }; + }, [getParsedDateRange]); + const buildQuery = useCallback(() => { return buildEsQuery(dataView, searchCriteria.query, [ ...searchCriteria.filters, @@ -116,15 +125,16 @@ export const useUnifiedSearch = () => { // Track telemetry event on query/filter/date changes useEffect(() => { - const dateRangeTimestamp = getDateRangeAsTimestamp(); + const parsedDateRange = getDateRangeAsTimestamp(); telemetry.reportHostsViewQuerySubmitted( - buildQuerySubmittedPayload({ ...searchCriteria, dateRangeTimestamp }) + buildQuerySubmittedPayload({ ...searchCriteria, parsedDateRange }) ); }, [getDateRangeAsTimestamp, searchCriteria, telemetry]); return { buildQuery, onSubmit, + getParsedDateRange, getDateRangeAsTimestamp, searchCriteria, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index 861f3c26472e8..bae9f2ed3f713 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -13,11 +13,13 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { enumeration } from '@kbn/securitysolution-io-ts-types'; import { FilterStateStore } from '@kbn/es-query'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useUrlState } from '../../../../utils/use_url_state'; import { useKibanaTimefilterTime, useSyncKibanaTimeFilterTime, } from '../../../../hooks/use_kibana_timefilter_time'; +import { DEFAULT_HOST_LIMIT, LOCAL_STORAGE_HOST_LIMIT_KEY } from '../constants'; const DEFAULT_QUERY = { language: 'kuery', @@ -32,6 +34,7 @@ const INITIAL_HOSTS_STATE: HostsState = { filters: [], panelFilters: [], dateRange: INITIAL_DATE_RANGE, + limit: DEFAULT_HOST_LIMIT, }; const reducer = (prevState: HostsState, params: HostsSearchPayload) => { @@ -45,9 +48,17 @@ const reducer = (prevState: HostsState, params: HostsSearchPayload) => { export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE); + const [localStorageHostLimit, setLocalStorageHostLimit] = useLocalStorage( + LOCAL_STORAGE_HOST_LIMIT_KEY, + INITIAL_HOSTS_STATE.limit + ); const [urlState, setUrlState] = useUrlState({ - defaultState: { ...INITIAL_HOSTS_STATE, dateRange: getTime() }, + defaultState: { + ...INITIAL_HOSTS_STATE, + dateRange: getTime(), + limit: localStorageHostLimit ?? INITIAL_HOSTS_STATE.limit, + }, decodeUrlState, encodeUrlState, urlStateKey: '_a', @@ -57,6 +68,9 @@ export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [search, setSearch] = useReducer(reducer, urlState); if (!deepEqual(search, urlState)) { setUrlState(search); + if (localStorageHostLimit !== search.limit) { + setLocalStorageHostLimit(search.limit); + } } useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, urlState.dateRange, (dateRange) => @@ -110,6 +124,7 @@ const HostsStateRT = rt.type({ panelFilters: HostsFiltersRT, query: HostsQueryStateRT, dateRange: StringDateRangeRT, + limit: rt.number, }); export type HostsState = rt.TypeOf; @@ -118,6 +133,7 @@ export type HostsSearchPayload = Partial; export type HostsStateUpdater = (params: HostsSearchPayload) => void; +export type StringDateRange = rt.TypeOf; export interface StringDateRangeTimestamp { from: number; to: number; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts index 6b948fb0da6c9..080b47f54d4da 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts @@ -7,7 +7,7 @@ import { Filter } from '@kbn/es-query'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { ALERT_STATUS_ALL } from './constants'; +import { ALERT_STATUS_ALL, HOST_LIMIT_OPTIONS } from './constants'; export type AlertStatus = | typeof ALERT_STATUS_ACTIVE @@ -19,3 +19,5 @@ export interface AlertStatusFilter { query?: Filter['query']; label: string; } + +export type HostLimitOptions = typeof HOST_LIMIT_OPTIONS[number]; diff --git a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts index bc9131d1d52fc..f39eaafdd039e 100644 --- a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts +++ b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts @@ -24,6 +24,9 @@ export const METADATA_AGGREGATION: Record Date: Mon, 24 Apr 2023 20:15:18 +0200 Subject: [PATCH 20/36] Fleet: allow Universal Profiling symbolizer permissions on indices (#155642) ## Summary For the introduction of the Universal Profiling symbolizer in Cloud, Fleet needs an update. The reason for Universal Profiling symbolizer to be different from other packages running via Fleet is that: 1. it ingests data into indicesm not only data-streams 2. it uses a non-conventional naming scheme for indices --- x-pack/plugins/fleet/common/constants/epm.ts | 1 + ...kage_policies_to_agent_permissions.test.ts | 92 +++++++++++++++++++ .../package_policies_to_agent_permissions.ts | 33 +++++++ 3 files changed, 126 insertions(+) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index a1d73b452cf72..2635dbc05399f 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -16,6 +16,7 @@ export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE = 'profiler_symbolizer'; export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture'; export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 1f5ea87c6d6f9..f093b20eaaeb4 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -13,6 +13,7 @@ import type { DataStreamMeta } from './package_policies_to_agent_permissions'; import { getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, + UNIVERSAL_PROFILING_PERMISSIONS, } from './package_policies_to_agent_permissions'; const packageInfoCache = new Map(); @@ -137,6 +138,56 @@ packageInfoCache.set('osquery_manager-0.3.0', { }, }, }); +packageInfoCache.set('profiler_symbolizer-8.8.0-preview', { + format_version: '2.7.0', + name: 'profiler_symbolizer', + title: 'Universal Profiling Symbolizer', + version: '8.8.0-preview', + license: 'basic', + description: + ' Fleet-wide, whole-system, continuous profiling with zero instrumentation. Symbolize native frames.', + type: 'integration', + release: 'beta', + categories: ['monitoring', 'elastic_stack'], + icons: [ + { + src: '/img/logo_profiling_symbolizer.svg', + title: 'logo symbolizer', + size: '32x32', + type: 'image/svg+xml', + }, + ], + owner: { github: 'elastic/profiling' }, + data_streams: [], + latestVersion: '8.8.0-preview', + notice: undefined, + status: 'not_installed', + assets: { + kibana: { + csp_rule_template: [], + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + tag: [], + osquery_pack_asset: [], + osquery_saved_query: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + ml_model: [], + }, + }, +}); describe('storedPackagePoliciesToAgentPermissions()', () => { it('Returns `undefined` if there are no package policies', async () => { @@ -363,6 +414,47 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }, }); }); + + it('Returns the Universal Profiling permissions for profiler_symbolizer package', async () => { + const packagePolicies: PackagePolicy[] = [ + { + id: 'package-policy-uuid-test-123', + name: 'test-policy', + namespace: '', + enabled: true, + package: { name: 'profiler_symbolizer', version: '8.8.0-preview', title: 'Test Package' }, + inputs: [ + { + type: 'pf-elastic-symbolizer', + enabled: true, + streams: [], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions( + packageInfoCache, + packagePolicies + ); + + expect(permissions).toMatchObject({ + 'package-policy-uuid-test-123': { + indices: [ + { + names: ['profiling-*'], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + }); + }); }); describe('getDataStreamPrivileges()', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index 02c44024421ce..f8cd73901e0d7 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE } from '../../../common/constants'; + import { getNormalizedDataStreams } from '../../../common/services'; import type { @@ -19,6 +21,16 @@ import { pkgToPkgKey } from '../epm/registry'; export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; +export const UNIVERSAL_PROFILING_PERMISSIONS = [ + 'auto_configure', + 'read', + 'create_doc', + 'create', + 'write', + 'index', + 'view_index_metadata', +]; + export async function storedPackagePoliciesToAgentPermissions( packageInfoCache: Map, packagePolicies?: PackagePolicy[] @@ -42,6 +54,12 @@ export async function storedPackagePoliciesToAgentPermissions( const pkg = packageInfoCache.get(pkgToPkgKey(packagePolicy.package))!; + // Special handling for Universal Profiling packages, as it does not use data streams _only_, + // but also indices that do not adhere to the convention. + if (pkg.name === FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE) { + return Promise.resolve(universalProfilingPermissions(packagePolicy.id)); + } + const dataStreams = getNormalizedDataStreams(pkg); if (!dataStreams || dataStreams.length === 0) { return [packagePolicy.name, undefined]; @@ -175,3 +193,18 @@ export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: s privileges, }; } + +async function universalProfilingPermissions(packagePolicyId: string): Promise<[string, any]> { + const profilingIndexPattern = 'profiling-*'; + return [ + packagePolicyId, + { + indices: [ + { + names: [profilingIndexPattern], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + ]; +} From 953437f05d26ee42ff70eb128e6646617bf7ba62 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 24 Apr 2023 20:21:39 +0200 Subject: [PATCH 21/36] [Enterprise Search] Fix wording on content settings page (#155383) ## Summary This updates the content on the content settings page. Screenshot 2023-04-20 at 14 02 02 --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../components/settings/settings.tsx | 37 ++++++++++--------- .../components/settings/settings_panel.tsx | 4 +- .../shared/doc_links/doc_links.ts | 3 ++ .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index d9fbd1dfc8554..dc75ebee9a5eb 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -153,6 +153,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { licenseManagement: `${ENTERPRISE_SEARCH_DOCS}license-management.html`, machineLearningStart: `${ENTERPRISE_SEARCH_DOCS}machine-learning-start.html`, mailService: `${ENTERPRISE_SEARCH_DOCS}mailer-configuration.html`, + mlDocumentEnrichment: `${ENTERPRISE_SEARCH_DOCS}document-enrichment.html`, start: `${ENTERPRISE_SEARCH_DOCS}start.html`, syncRules: `${ENTERPRISE_SEARCH_DOCS}sync-rules.html`, troubleshootSetup: `${ENTERPRISE_SEARCH_DOCS}troubleshoot-setup.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 92dc64e916644..efe5e95f238d0 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -138,6 +138,7 @@ export interface DocLinks { readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; + readonly mlDocumentEnrichment: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx index a1bbe6c39956f..840010d3aa219 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx @@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + import { docLinks } from '../../../shared/doc_links'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; @@ -36,6 +38,21 @@ export const Settings: React.FC = () => { }), ]} pageHeader={{ + description: ( + + {i18n.translate('xpack.enterpriseSearch.content.settings.ingestLink', { + defaultMessage: 'ingest pipelines', + })} + + ), + }} + /> + ), pageTitle: i18n.translate('xpack.enterpriseSearch.content.settings.headerTitle', { defaultMessage: 'Content Settings', }), @@ -69,19 +86,12 @@ export const Settings: React.FC = () => { 'xpack.enterpriseSearch.content.settings.contentExtraction.description', { defaultMessage: - 'Allow all ingestion mechanisms on your Enterprise Search deployment to extract searchable content from binary files, like PDFs and Word documents. This setting applies to all new Elasticsearch indices created by an Enterprise Search ingestion mechanism.', + 'Extract searchable content from binary files, like PDFs and Word documents.', } )} label={i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.label', { defaultMessage: 'Content extraction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.link', { - defaultMessage: 'Learn more about content extraction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -105,13 +115,6 @@ export const Settings: React.FC = () => { label={i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.label', { defaultMessage: 'Whitespace reduction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.link', { - defaultMessage: 'Learn more about whitespace reduction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -139,9 +142,9 @@ export const Settings: React.FC = () => { defaultMessage: 'ML Inference', })} link={ - + {i18n.translate('xpack.enterpriseSearch.content.settings.mlInference.link', { - defaultMessage: 'Learn more about content extraction', + defaultMessage: 'Learn more about document enrichment with ML', })} } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx index fc0f3cca3e06c..673861d80e6ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; interface SettingsPanelProps { description: string; label: string; - link: React.ReactNode; + link?: React.ReactNode; onChange: (event: EuiSwitchEvent) => void; title: string; value: boolean; @@ -61,7 +61,7 @@ export const SettingsPanel: React.FC = ({ - {link} + {link && {link}} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 938aaa88d1bdf..cb6663eebf6ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -92,6 +92,7 @@ class DocLinks { public languageClients: string; public licenseManagement: string; public machineLearningStart: string; + public mlDocumentEnrichment: string; public pluginsIngestAttachment: string; public queryDsl: string; public searchUIAppSearch: string; @@ -219,6 +220,7 @@ class DocLinks { this.languageClients = ''; this.licenseManagement = ''; this.machineLearningStart = ''; + this.mlDocumentEnrichment = ''; this.pluginsIngestAttachment = ''; this.queryDsl = ''; this.searchUIAppSearch = ''; @@ -340,6 +342,7 @@ class DocLinks { this.languageClients = docLinks.links.enterpriseSearch.languageClients; this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; this.machineLearningStart = docLinks.links.enterpriseSearch.machineLearningStart; + this.mlDocumentEnrichment = docLinks.links.enterpriseSearch.mlDocumentEnrichment; this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment; this.queryDsl = docLinks.links.query.queryDsl; this.searchUIAppSearch = docLinks.links.searchUI.appSearch; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d09b84345903e..f6610dba543b2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12711,7 +12711,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "Statut", "xpack.enterpriseSearch.content.settings.breadcrumb": "Paramètres", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "Extraction du contenu", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "En savoir plus sur l'extraction de contenu", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "Autoriser tous les mécanismes d'ingestion de votre déploiement Enterprise Search à extraire le contenu interrogeable des fichiers binaires tels que les documents PDF et Word. Ce paramètre s'applique à tous les nouveaux index Elasticsearch créés par un mécanisme d'ingestion Enterprise Search.", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "Vous pouvez également activer ou désactiver cette fonctionnalité pour un index spécifique sur la page de configuration de l'index.", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "Extraction de contenu de l'ensemble du déploiement", @@ -12725,7 +12724,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "Réduction d'espaces sur l'ensemble du déploiement", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "La réduction d'espaces supprimera le contenu de texte intégral des espaces par défaut.", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "Réduction d'espaces", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "En savoir plus sur la réduction d'espaces", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "Supprimer le document", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "Métadonnées du document", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "Inclure tout le reste à partir de cette source", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0fc77aa25087d..91baf79cfaaa0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12710,7 +12710,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "ステータス", "xpack.enterpriseSearch.content.settings.breadcrumb": "設定", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "コンテンツ抽出", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "コンテンツ抽出の詳細", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "エンタープライズ サーチデプロイですべてのインジェストメソッドで、PDFやWordドキュメントなどのバイナリファイルから検索可能なコンテンツを抽出できます。この設定は、エンタープライズ サーチインジェストメカニズムで作成されたすべての新しいElasticsearchインデックスに適用されます。", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "インデックスの構成ページで、特定のインデックスに対して、この機能を有効化または無効化することもできます。", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "デプロイレベルのコンテンツ抽出", @@ -12724,7 +12723,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "デプロイレベルの空白削除", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "空白削除では、デフォルトで全文コンテンツから空白を削除します。", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白削除", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "空白削除の詳細", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "ドキュメントを削除", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "ドキュメントメタデータ", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "このソースの他のすべての項目を含める", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1ce46a2242307..ed0f6a5c2fedb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12711,7 +12711,6 @@ "xpack.enterpriseSearch.content.searchIndices.syncStatus.columnTitle": "状态", "xpack.enterpriseSearch.content.settings.breadcrumb": "设置", "xpack.enterpriseSearch.content.settings.contactExtraction.label": "内容提取", - "xpack.enterpriseSearch.content.settings.contactExtraction.link": "详细了解内容提取", "xpack.enterpriseSearch.content.settings.contentExtraction.description": "允许您的 Enterprise Search 部署上的所有采集机制从 PDF 和 Word 文档等二进制文件中提取可搜索内容。此设置适用于由 Enterprise Search 采集机制创建的所有新 Elasticsearch 索引。", "xpack.enterpriseSearch.content.settings.contentExtraction.descriptionTwo": "您还可以在索引的配置页面针对特定索引启用或禁用此功能。", "xpack.enterpriseSearch.content.settings.contentExtraction.title": "部署广泛内容提取", @@ -12725,7 +12724,6 @@ "xpack.enterpriseSearch.content.settings.whitespaceReduction.deploymentHeaderTitle": "部署广泛的空白缩减", "xpack.enterpriseSearch.content.settings.whiteSpaceReduction.description": "默认情况下,空白缩减将清除空白的全文本内容。", "xpack.enterpriseSearch.content.settings.whitespaceReduction.label": "空白缩减", - "xpack.enterpriseSearch.content.settings.whitespaceReduction.link": "详细了解空白缩减", "xpack.enterpriseSearch.content.shared.result.header.metadata.deleteDocument": "删除文档", "xpack.enterpriseSearch.content.shared.result.header.metadata.title": "文档元数据", "xpack.enterpriseSearch.content.sources.basicRulesTable.includeEverythingMessage": "包括来自此源的所有其他内容", From 4382e1cf3227e966995c2486043d34f1b875d7d9 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 24 Apr 2023 15:14:30 -0400 Subject: [PATCH 22/36] [ResponseOps] adds mustache lambdas and array.asJSON (#150572) partially resolves some issues in https://github.com/elastic/kibana/issues/84217 Adds Mustache lambdas for alerting actions to format dates with `{{#FormatDate}}`, evaluate math expressions with `{{#EvalMath}}`, and provide easier JSON formatting with `{{#ParseHjson}}` and a new `asJSON` property added to arrays. --- .../server/lib/mustache_lambdas.test.ts | 201 +++++++++ .../actions/server/lib/mustache_lambdas.ts | 114 +++++ .../server/lib/mustache_renderer.test.ts | 11 + .../actions/server/lib/mustache_renderer.ts | 9 +- x-pack/plugins/actions/tsconfig.json | 3 +- .../server/slack_simulation.ts | 2 +- .../server/webhook_simulation.ts | 4 +- .../plugins/alerts/server/alert_types.ts | 1 + .../alerting/group4/mustache_templates.ts | 421 ++++++++---------- 9 files changed, 534 insertions(+), 232 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts create mode 100644 x-pack/plugins/actions/server/lib/mustache_lambdas.ts diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts new file mode 100644 index 0000000000000..6f67c4dd39ea8 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts @@ -0,0 +1,201 @@ +/* + * 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 dedent from 'dedent'; + +import { renderMustacheString } from './mustache_renderer'; + +describe('mustache lambdas', () => { + describe('FormatDate', () => { + it('date with defaults is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 03:52pm'); + }); + + it('date with a time zone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ; America/New_York {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 10:52am'); + }); + + it('date with a format is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ;; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual( + 'Tuesday Nov 29th 2022 15:52:44.000' + ); + }); + + it('date with a format and timezone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}};America/New_York;dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'Tuesday Nov 29th 2022 10:52:44.000' + ); + }); + + it('empty date produces error', () => { + const timeStamp = ''; + const template = dedent` + {{#FormatDate}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}} {{/FormatDate}}": date is empty' + ); + }); + + it('invalid date produces error', () => { + const timeStamp = 'this is not a d4t3'; + const template = dedent` + {{#FormatDate}}{{timeStamp}}{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}}{{/FormatDate}}": invalid date "this is not a d4t3"' + ); + }); + + it('invalid timezone produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}}": unknown timeZone value "NotATime Zone!"' + ); + }); + + it('invalid format produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};;garbage{{/FormatDate}} + `.trim(); + + // not clear how to force an error, it pretty much does something with + // ANY string + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'gamrbamg2' // a => am/pm (so am here); e => day of week + ); + }); + }); + + describe('EvalMath', () => { + it('math is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} 1 + 0 {{/EvalMath}} + {{#EvalMath}} 1 + context.a.b {{/EvalMath}} + {{#context}} + {{#EvalMath}} 1 + c.d {{/EvalMath}} + {{/context}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual(`1\n2\n3\n`); + }); + + it('invalid expression produces error', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual( + `error rendering mustache template "{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}": error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.` + ); + }); + }); + + describe('ParseHJson', () => { + it('valid Hjson is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const hjson = ` + { + # specify rate in requests/second (because comments are helpful!) + rate: 1000 + + a: {{context.a}} + a_b: {{context.a.b}} + c: {{context.c}} + c_d: {{context.c.d}} + + # list items can be separated by lines, or commas, and trailing + # commas permitted + list: [ + 1 2 + 3 + 4,5,6, + ] + }`; + const template = dedent` + {{#ParseHjson}} ${hjson} {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(JSON.parse(result)).toMatchInlineSnapshot(` + Object { + "a": Object { + "b": 1, + }, + "a_b": 1, + "c": Object { + "d": 2, + }, + "c_d": 2, + "list": Array [ + "1 2", + 3, + 4, + 5, + 6, + ], + "rate": 1000, + } + `); + }); + + it('renders an error message on parse errors', () => { + const template = dedent` + {{#ParseHjson}} [1,2,3,,] {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, {}, 'none'); + expect(result).toMatch(/^error rendering mustache template .*/); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts new file mode 100644 index 0000000000000..62ba5621e0e1e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts @@ -0,0 +1,114 @@ +/* + * 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 * as tinymath from '@kbn/tinymath'; +import { parse as hjsonParse } from 'hjson'; + +import moment from 'moment-timezone'; + +type Variables = Record; + +const DefaultDateTimeZone = 'UTC'; +const DefaultDateFormat = 'YYYY-MM-DD hh:mma'; + +export function getMustacheLambdas(): Variables { + return getLambdas(); +} + +const TimeZoneSet = new Set(moment.tz.names()); + +type RenderFn = (text: string) => string; + +function getLambdas() { + return { + EvalMath: () => + // mustache invokes lamdas with `this` set to the current "view" (variables) + function (this: Variables, text: string, render: RenderFn) { + return evalMath(this, render(text.trim())); + }, + ParseHjson: () => + function (text: string, render: RenderFn) { + return parseHjson(render(text.trim())); + }, + FormatDate: () => + function (text: string, render: RenderFn) { + const dateString = render(text.trim()).trim(); + return formatDate(dateString); + }, + }; +} + +function evalMath(vars: Variables, o: unknown): string { + const expr = `${o}`; + try { + const result = tinymath.evaluate(expr, vars); + return `${result}`; + } catch (err) { + throw new Error(`error evaluating tinymath expression "${expr}": ${err.message}`); + } +} + +function parseHjson(o: unknown): string { + const hjsonObject = `${o}`; + let object: unknown; + + try { + object = hjsonParse(hjsonObject); + } catch (err) { + throw new Error(`error parsing Hjson "${hjsonObject}": ${err.message}`); + } + + return JSON.stringify(object); +} + +function formatDate(dateString: unknown): string { + const { date, timeZone, format } = splitDateString(`${dateString}`); + + if (date === '') { + throw new Error(`date is empty`); + } + + if (isNaN(new Date(date).valueOf())) { + throw new Error(`invalid date "${date}"`); + } + + let mDate: moment.Moment; + try { + mDate = moment(date); + if (!mDate.isValid()) { + throw new Error(`date is invalid`); + } + } catch (err) { + throw new Error(`error evaluating moment date "${date}": ${err.message}`); + } + + if (!TimeZoneSet.has(timeZone)) { + throw new Error(`unknown timeZone value "${timeZone}"`); + } + + try { + mDate.tz(timeZone); + } catch (err) { + throw new Error(`error evaluating moment timeZone "${timeZone}": ${err.message}`); + } + + try { + return mDate.format(format); + } catch (err) { + throw new Error(`error evaluating moment format "${format}": ${err.message}`); + } +} + +function splitDateString(dateString: string) { + const parts = dateString.split(';', 3).map((s) => s.trim()); + const [date = '', timeZone = '', format = ''] = parts; + return { + date, + timeZone: timeZone || DefaultDateTimeZone, + format: format || DefaultDateFormat, + }; +} diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index 964a793d8f81c..3a02ce0d1a983 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -58,6 +58,12 @@ describe('mustache_renderer', () => { expect(renderMustacheString('{{f.g}}', variables, escape)).toBe('3'); expect(renderMustacheString('{{f.h}}', variables, escape)).toBe(''); expect(renderMustacheString('{{i}}', variables, escape)).toBe('42,43,44'); + + if (escape === 'markdown') { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('\\[42,43,44\\]'); + } else { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('[42,43,44]'); + } }); } @@ -339,6 +345,11 @@ describe('mustache_renderer', () => { const expected = '1 - {"c":2,"d":[3,4]} -- 5,{"f":6,"g":7}'; expect(renderMustacheString('{{a}} - {{b}} -- {{e}}', deepVariables, 'none')).toEqual(expected); + + expect(renderMustacheString('{{e}}', deepVariables, 'none')).toEqual('5,{"f":6,"g":7}'); + expect(renderMustacheString('{{e.asJSON}}', deepVariables, 'none')).toEqual( + '[5,{"f":6,"g":7}]' + ); }); describe('converting dot variables', () => { diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.ts index fc4381fa0c9c3..37713167e9a34 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.ts @@ -7,8 +7,10 @@ import Mustache from 'mustache'; import { isString, isPlainObject, cloneDeepWith, merge } from 'lodash'; +import { getMustacheLambdas } from './mustache_lambdas'; export type Escape = 'markdown' | 'slack' | 'json' | 'none'; + type Variables = Record; // return a rendered mustache template with no escape given the specified variables and escape @@ -25,11 +27,13 @@ export function renderMustacheStringNoEscape(string: string, variables: Variable // return a rendered mustache template given the specified variables and escape export function renderMustacheString(string: string, variables: Variables, escape: Escape): string { const augmentedVariables = augmentObjectVariables(variables); + const lambdas = getMustacheLambdas(); + const previousMustacheEscape = Mustache.escape; Mustache.escape = getEscape(escape); try { - return Mustache.render(`${string}`, augmentedVariables); + return Mustache.render(`${string}`, { ...lambdas, ...augmentedVariables }); } catch (err) { // log error; the mustache code does not currently leak variables return `error rendering mustache template "${string}": ${err.message}`; @@ -98,6 +102,9 @@ function addToStringDeep(object: unknown): void { // walk arrays, but don't add a toString() as mustache already does something if (Array.isArray(object)) { + // instead, add an asJSON() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (object as any).asJSON = () => JSON.stringify(object); object.forEach((element) => addToStringDeep(element)); return; } diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 77ef11e88bfe3..8c253cb644fee 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -33,7 +33,8 @@ "@kbn/logging-mocks", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/safer-lodash-set", - "@kbn/core-http-server-mocks" + "@kbn/core-http-server-mocks", + "@kbn/tinymath", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts index f1a67c568b67f..eee078591a3a7 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts @@ -35,7 +35,7 @@ export async function initPlugin() { } // store a message that was posted to be remembered - const match = text.match(/^message (.*)$/); + const match = text.match(/^message ([\S\s]*)$/); if (match) { messages.push(match[1]); response.statusCode = 200; diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts index f3f0aa8f6469b..baa6ee80a8e53 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts @@ -79,7 +79,7 @@ function createServerCallback() { } // store a payload that was posted to be remembered - const match = data.match(/^payload (.*)$/); + const match = data.match(/^payload ([\S\s]*)$/); if (match) { payloads.push(match[1]); response.statusCode = 200; @@ -89,6 +89,8 @@ function createServerCallback() { response.statusCode = 400; response.end(`unexpected body ${data}`); + // eslint-disable-next-line no-console + console.log(`webhook simulator received unexpected body: ${data}`); return; }); } else { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index 60e7e82966864..6d716b5d3c235 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -47,6 +47,7 @@ export const DeepContextVariables = { arrayI: [44, 45], nullJ: null, undefinedK: undefined, + dateL: '2023-04-20T04:13:17.858Z', }; function getAlwaysFiringAlertType() { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts index 1eb7d99b93bda..764dd728b1347 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts @@ -14,13 +14,16 @@ import http from 'http'; import getPort from 'get-port'; -import { URL, format as formatUrl } from 'url'; import axios from 'axios'; import expect from '@kbn/expect'; import { getWebhookServer, getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin'; import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData as getCoreTestRuleData, + ObjectRemover, +} from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -32,8 +35,10 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon const objectRemover = new ObjectRemover(supertest); let webhookSimulatorURL: string = ''; let webhookServer: http.Server; + let webhookConnector: any; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let slackConnector: any; before(async () => { let availablePort: number; @@ -42,6 +47,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon availablePort = await getPort({ port: 9000 }); webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + webhookConnector = await createWebhookConnector(webhookSimulatorURL); slackServer = await getSlackServer(); availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); @@ -49,6 +55,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.listen(availablePort); } slackSimulatorURL = `http://localhost:${availablePort}`; + slackConnector = await createSlackConnector(slackSimulatorURL); }); after(async () => { @@ -57,219 +64,177 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.close(); }); - it('should handle escapes in webhook', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing mustache escapes for webhook', - connector_type_id: '.webhook', - secrets: {}, - config: { - headers: { - 'Content-Type': 'text/plain', - }, - url, + describe('escaping', () => { + it('should handle escapes in webhook', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); + }); + + it('should handle escapes in slack', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = + '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); + }); + + it('should handle context variable object expansion', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = '{{context.deep}}'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be( + '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null,"dateL":"2023-04-20T04:13:17.858Z"}' ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); - }); - - it('should handle escapes in slack', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: "testing backtic'd mustache escapes for slack", - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + }); + + it('should render kibanaBaseUrl as empty string since not configured', async () => { + const template = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = - '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be('kibanaBaseUrl: ""'); + }); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for slack', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); - }); - - it('should handle context variable object expansion', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + it('should render action variables in rule action', async () => { + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const DeepContextVariables - const varsTemplate = '{{context.deep}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable expansion', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be( + `old id variable: ${rule.id}, new id variable: ${rule.id}, old name variable: ${rule.name}, new name variable: ${rule.name}` ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}' - ); + }); }); - it('should render kibanaBaseUrl as empty string since not configured', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + describe('lambdas', () => { + it('should handle ParseHjson', async () => { + const template = `{{#ParseHjson}} { + ruleId: {{rule.id}} + ruleName: {{rule.name}} + } {{/ParseHjson}}`; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`{"ruleId":"${rule.id}","ruleName":"testing mustache templates"}`); + }); + + it('should handle asJSON', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep.objectA}} + {{{arrayC}}} {{{arrayC.asJSON}}} + {{/context.deep.objectA}} + `; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + const expected1 = `{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}`; + const expected2 = `[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}]`; + expect(body.trim()).to.be(`${expected1} ${expected2}`); + }); + + it('should handle EvalMath', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}avg({{arrayI.0}}, {{arrayI.1}})/100 => {{#EvalMath}} + round((arrayI[0] + arrayI[1]) / 2 / 100, 2) + {{/EvalMath}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be(`avg(44, 45)/100 => 0.45`); + }); + + it('should handle FormatDate', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}{{#FormatDate}} + {{{dateL}}} ; America/New_York; dddd MMM Do YYYY HH:mm:ss + {{/FormatDate}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body.trim()).to.be(`Thursday Apr 20th 2023 00:13:17`); + }); + }); - const alertResponse = await supertest + async function createRule(action: any) { + const ruleResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable kibanaBaseUrl', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be('kibanaBaseUrl: ""'); - }); + .send(getTestRuleData({ actions: [action] })); + expect(ruleResponse.status).to.eql(200); + const rule = ruleResponse.body; + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + return rule; + } - it('should render action variables in rule action', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest + async function createWebhookConnector(url: string) { + const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'test') .send({ - name: 'testing action variable rendering', + name: 'testing mustache for webhook', connector_type_id: '.webhook', secrets: {}, config: { @@ -279,42 +244,30 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon url, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - `old id variable: ${createdAlert.id}, new id variable: ${createdAlert.id}, old name variable: ${createdAlert.name}, new name variable: ${createdAlert.name}` - ); - }); + return connector; + } + + async function createSlackConnector(url: string) { + const createResponse = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'test') + .send({ + name: 'testing mustache for slack', + connector_type_id: '.slack', + secrets: { + webhookUrl: url, + }, + }); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); + + return connector; + } }); async function waitForActionBody(url: string, id: string): Promise { @@ -322,7 +275,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon expect(response.status).to.eql(200); for (const datum of response.data) { - const match = datum.match(/^(.*) - (.*)$/); + const match = datum.match(/^(.*) - ([\S\s]*)$/); if (match == null) continue; if (match[1] === id) return match[2]; @@ -331,3 +284,15 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon throw new Error(`no action body posted yet for id ${id}`); } } + +function getTestRuleData(overrides: any) { + const defaults = { + name: 'testing mustache templates', + rule_type_id: 'test.patternFiring', + params: { + pattern: { instance: [true] }, + }, + }; + + return getCoreTestRuleData({ ...overrides, ...defaults }); +} From e46cb1ab8a98af3cbad8c6affcd3477cea9abc9d Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Mon, 24 Apr 2023 14:16:43 -0500 Subject: [PATCH 23/36] [Enterprise Search][Search Applications] introduce content page (#155632) ## Summary Introduced a Content page for Search Application that combined the Indices and Schema pages into a single page with tabs ### Screenshots image image image ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../engine/engine_connect/engine_connect.tsx | 4 +- .../components/engine/engine_indices.tsx | 206 +++++++--------- .../components/engine/engine_schema.tsx | 227 ++++++++---------- .../components/engine/engine_view.tsx | 12 +- .../engine/search_application_content.tsx | 156 ++++++++++++ .../enterprise_search_content/routes.ts | 8 +- .../applications/shared/layout/nav.test.tsx | 11 +- .../public/applications/shared/layout/nav.tsx | 19 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 11 files changed, 372 insertions(+), 283 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/search_application_content.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx index 2dd55304b6035..a6c29285f4f66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; import { KibanaLogic } from '../../../../shared/kibana'; import { - SEARCH_APPLICATION_CONNECT_PATH, + SEARCH_APPLICATION_CONTENT_PATH, EngineViewTabs, SearchApplicationConnectTabs, } from '../../../routes'; @@ -55,7 +55,7 @@ export const EngineConnect: React.FC = () => { }>(); const onTabClick = (tab: SearchApplicationConnectTabs) => () => { KibanaLogic.values.navigateToUrl( - generateEncodedPath(SEARCH_APPLICATION_CONNECT_PATH, { + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { engineName, connectTabId: tab, }) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx index 4bcd3fcf4f215..285846f7ccb1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx @@ -11,7 +11,6 @@ import { useActions, useValues } from 'kea'; import { EuiBasicTableColumn, - EuiButton, EuiCallOut, EuiConfirmModal, EuiIcon, @@ -32,24 +31,15 @@ import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic'; -import { SEARCH_INDEX_PATH, EngineViewTabs } from '../../routes'; +import { SEARCH_INDEX_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; - -import { AddIndicesFlyout } from './add_indices_flyout'; import { EngineIndicesLogic } from './engine_indices_logic'; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.indices.pageTitle', { - defaultMessage: 'Indices', -}); - export const EngineIndices: React.FC = () => { const subduedBackground = useEuiBackgroundColor('subdued'); const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); - const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } = - useValues(EngineIndicesLogic); - const { removeIndexFromEngine, openAddIndicesFlyout, closeAddIndicesFlyout } = - useActions(EngineIndicesLogic); + const { engineData } = useValues(EngineIndicesLogic); + const { removeIndexFromEngine } = useActions(EngineIndicesLogic); const { navigateToUrl } = useValues(KibanaLogic); const [removeIndexConfirm, setConfirmRemoveIndex] = useState(null); @@ -177,116 +167,92 @@ export const EngineIndices: React.FC = () => { }, ]; return ( - + {hasUnknownIndices && ( + <> + - {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { - defaultMessage: 'Add new indices', - })} - , - ], - }} - engineName={engineName} - > - <> - {hasUnknownIndices && ( - <> - + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', + { + defaultMessage: + 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove indices that should no longer be used by this search application.', + } )} - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', - { - defaultMessage: - 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove those that should no longer be used by this search application.', - } - )} -

-
- - - )} - { - if (index.health === 'unknown') { - return { style: { backgroundColor: subduedBackground } }; - } +

+
+ + + )} + { + if (index.health === 'unknown') { + return { style: { backgroundColor: subduedBackground } }; + } - return {}; - }} - search={{ - box: { - incremental: true, - placeholder: i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', - { defaultMessage: 'Filter indices' } - ), - schema: true, - }, + return {}; + }} + search={{ + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', + { defaultMessage: 'Filter indices' } + ), + schema: true, + }, + }} + pagination + sorting + /> + {removeIndexConfirm !== null && ( + setConfirmRemoveIndex(null)} + onConfirm={() => { + removeIndexFromEngine(removeIndexConfirm); + setConfirmRemoveIndex(null); + sendEnterpriseSearchTelemetry({ + action: 'clicked', + metric: 'entSearchContent-engines-indices-removeIndexConfirm', + }); }} - pagination - sorting - /> - {removeIndexConfirm !== null && ( - setConfirmRemoveIndex(null)} - onConfirm={() => { - removeIndexFromEngine(removeIndexConfirm); - setConfirmRemoveIndex(null); - sendEnterpriseSearchTelemetry({ - action: 'clicked', - metric: 'entSearchContent-engines-indices-removeIndexConfirm', - }); - }} - title={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', - { defaultMessage: 'Remove this index from the Search Application' } - )} - buttonColor="danger" - cancelButtonText={CANCEL_BUTTON_LABEL} - confirmButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', - { - defaultMessage: 'Yes, Remove This Index', - } - )} - defaultFocusedButton="confirm" - maxWidth - > - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', - { - defaultMessage: - "This won't delete the index. You may add it back to this search application at a later time.", - } - )} -

-
-
- )} - {addIndicesFlyoutOpen && } - -
+ title={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', + { defaultMessage: 'Remove this index from the search application' } + )} + buttonColor="danger" + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', + { + defaultMessage: 'Yes, Remove This Index', + } + )} + defaultFocusedButton="confirm" + maxWidth + > + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', + { + defaultMessage: + "This won't delete the index. You may add it back to this search application at a later time.", + } + )} +

+
+ + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx index 4ceba9cccb142..fd10624188ff0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx @@ -42,8 +42,7 @@ import { docLinks } from '../../../shared/doc_links'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { EngineViewTabs, SEARCH_INDEX_TAB_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; +import { SEARCH_INDEX_TAB_PATH } from '../../routes'; import { EngineIndicesLogic } from './engine_indices_logic'; @@ -153,10 +152,6 @@ const SchemaFieldDetails: React.FC<{ schemaField: SchemaField }> = ({ schemaFiel ); }; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.schema.pageTitle', { - defaultMessage: 'Schema', -}); - export const EngineSchema: React.FC = () => { const { engineName } = useValues(EngineIndicesLogic); const [onlyShowConflicts, setOnlyShowConflicts] = useState(false); @@ -348,125 +343,115 @@ export const EngineSchema: React.FC = () => { ); return ( - - <> - - - - - - {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { - defaultMessage: 'Filter By', - })} - - - setIsFilterByPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downCenter" + <> + + + + + + {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { + defaultMessage: 'Filter By', + })} + + + setIsFilterByPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downCenter" + > + setSelectedEsFieldTypes(options)} > - ( +
+ {search} + {list} +
+ )} +
+ + + setSelectedEsFieldTypes(esFieldTypes)} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', { - defaultMessage: 'Filter list ', + defaultMessage: 'Clear all ', } - ), - }} - options={selectedEsFieldTypes} - onChange={(options) => setSelectedEsFieldTypes(options)} - > - {(list, search) => ( -
- {search} - {list} -
- )} -
- - - setSelectedEsFieldTypes(esFieldTypes)} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', - { - defaultMessage: 'Clear all ', - } - )} - - - -
-
-
+ )} + +
+ +
+
- - - {totalConflictsHiddenByTypeFilters > 0 && ( - - } - color="danger" - iconType="iInCircle" - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', - { - defaultMessage: - 'In order to see all field conflicts you must clear your field filters', - } - )} -

- setSelectedEsFieldTypes(esFieldTypes)}> - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', - { - defaultMessage: 'Clear filters ', - } - )} - -
- )}
- -
+ + + {totalConflictsHiddenByTypeFilters > 0 && ( + + } + color="danger" + iconType="iInCircle" + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', + { + defaultMessage: + 'In order to see all field conflicts you must clear your field filters', + } + )} +

+ setSelectedEsFieldTypes(esFieldTypes)}> + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', + { + defaultMessage: 'Clear filters ', + } + )} + +
+ )} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx index 35e56f825d33a..6bbfe9b1de967 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx @@ -17,9 +17,11 @@ import { Status } from '../../../../../common/types/api'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH, + SEARCH_APPLICATION_CONTENT_PATH, SEARCH_APPLICATION_CONNECT_PATH, EngineViewTabs, SearchApplicationConnectTabs, + SearchApplicationContentTabs, } from '../../routes'; import { DeleteEngineModal } from '../engines/delete_engine_modal'; @@ -27,11 +29,10 @@ import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_temp import { EngineConnect } from './engine_connect/engine_connect'; import { EngineError } from './engine_error'; -import { EngineIndices } from './engine_indices'; -import { EngineSchema } from './engine_schema'; import { EngineSearchPreview } from './engine_search_preview/engine_search_preview'; import { EngineViewLogic } from './engine_view_logic'; import { EngineHeaderDocsAction } from './header_docs_action'; +import { SearchApplicationContent } from './search_application_content'; export const EngineView: React.FC = () => { const { fetchEngine, closeDeleteEngineModal } = useActions(EngineViewLogic); @@ -74,8 +75,11 @@ export const EngineView: React.FC = () => { path={`${ENGINE_PATH}/${EngineViewTabs.PREVIEW}`} component={EngineSearchPreview} /> - - + + { + switch (tabId) { + case SearchApplicationContentTabs.INDICES: + return INDICES_TAB_TITLE; + case SearchApplicationContentTabs.SCHEMA: + return SCHEMA_TAB_TITLE; + default: + return tabId; + } +}; + +const ContentTabs: string[] = Object.values(SearchApplicationContentTabs); + +export const SearchApplicationContent = () => { + const { engineName, isLoadingEngine } = useValues(EngineViewLogic); + const { addIndicesFlyoutOpen } = useValues(EngineIndicesLogic); + const { closeAddIndicesFlyout, openAddIndicesFlyout } = useActions(EngineIndicesLogic); + const { contentTabId = SearchApplicationContentTabs.INDICES } = useParams<{ + contentTabId?: string; + }>(); + + if (!ContentTabs.includes(contentTabId)) { + return ( + + + + ); + } + + const onTabClick = (tab: SearchApplicationContentTabs) => () => { + KibanaLogic.values.navigateToUrl( + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { + contentTabId: tab, + engineName, + }) + ); + }; + + return ( + + KibanaLogic.values.navigateToUrl( + generateEncodedPath(ENGINE_PATH, { + engineName, + }) + ), + text: ( + <> + {engineName} + + ), + }, + ], + pageTitle, + rightSideItems: [ + + {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { + defaultMessage: 'Add new indices', + })} + , + ], + tabs: [ + { + isSelected: contentTabId === SearchApplicationContentTabs.INDICES, + label: INDICES_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.INDICES), + }, + { + isSelected: contentTabId === SearchApplicationContentTabs.SCHEMA, + label: SCHEMA_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.SCHEMA), + }, + ], + }} + engineName={engineName} + > + {contentTabId === SearchApplicationContentTabs.INDICES && } + {contentTabId === SearchApplicationContentTabs.SCHEMA && } + {addIndicesFlyoutOpen && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index ea5672222014f..7b5bc7bf286f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -29,8 +29,7 @@ export const ENGINES_PATH = `${ROOT_PATH}engines`; export enum EngineViewTabs { PREVIEW = 'preview', - INDICES = 'indices', - SCHEMA = 'schema', + CONTENT = 'content', CONNECT = 'connect', } export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; @@ -40,5 +39,10 @@ export const SEARCH_APPLICATION_CONNECT_PATH = `${ENGINE_PATH}/${EngineViewTabs. export enum SearchApplicationConnectTabs { API = 'api', } +export const SEARCH_APPLICATION_CONTENT_PATH = `${ENGINE_PATH}/${EngineViewTabs.CONTENT}/:contentTabId`; +export enum SearchApplicationContentTabs { + INDICES = 'indices', + SCHEMA = 'schema', +} export const ML_MANAGE_TRAINED_MODELS_PATH = '/app/ml/trained_models'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 11458565f781a..dcb343a2beec4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -290,14 +290,9 @@ describe('useEnterpriseSearchEngineNav', () => { name: 'Preview', }, { - href: `/app/enterprise_search/content/engines/${engineName}/indices`, - id: 'enterpriseSearchEngineIndices', - name: 'Indices', - }, - { - href: `/app/enterprise_search/content/engines/${engineName}/schema`, - id: 'enterpriseSearchEngineSchema', - name: 'Schema', + href: `/app/enterprise_search/content/engines/${engineName}/content`, + id: 'enterpriseSearchApplicationsContent', + name: 'Content', }, { href: `/app/enterprise_search/content/engines/${engineName}/connect`, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index f06e32b9e30c8..7fbc354ff725f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -204,23 +204,14 @@ export const useEnterpriseSearchEngineNav = (engineName?: string, isEmptyState?: }), }, { - id: 'enterpriseSearchEngineIndices', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.indicesTitle', { - defaultMessage: 'Indices', + id: 'enterpriseSearchApplicationsContent', + name: i18n.translate('xpack.enterpriseSearch.nav.engine.contentTitle', { + defaultMessage: 'Content', }), ...generateNavLink({ shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.INDICES}`, - }), - }, - { - id: 'enterpriseSearchEngineSchema', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.schemaTitle', { - defaultMessage: 'Schema', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.SCHEMA}`, + shouldShowActiveForSubroutes: true, + to: `${enginePath}/${EngineViewTabs.CONTENT}`, }), }, { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f6610dba543b2..f7cf6a952a796 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -12229,7 +12229,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "Nombre de documents", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "Intégrité des index", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "Nom de l'index", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "Index", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "L'index ne sera pas supprimé. Vous pourrez l'ajouter de nouveau à ce moteur ultérieurement.", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "Oui, retirer cet index", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "Retirer cet index du moteur", @@ -12237,7 +12236,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "Documents :", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "Nom du champ", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "Type du champ", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "Schéma", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "Oui, supprimer ce moteur ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "La suppression de votre moteur ne pourra pas être annulée. Vos index ne seront pas affectés. ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "Supprimer définitivement ce moteur ?", @@ -13091,8 +13089,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "Paramètres", "xpack.enterpriseSearch.nav.contentTitle": "Contenu", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "Index", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "Schéma", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "Aperçu", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "Expériences de recherche", "xpack.enterpriseSearch.nav.searchIndicesTitle": "Index", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 91baf79cfaaa0..2e5fdf22d200b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12228,7 +12228,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "ドキュメント数", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "インデックス正常性", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "インデックス名", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "インデックス", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "インデックスは削除されません。後からこのエンジンに追加することができます。", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "はい。このインデックスを削除します", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "このインデックスをエンジンから削除", @@ -12236,7 +12235,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "ドキュメント:", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "フィールド名", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "フィールド型", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "スキーマ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "はい。このエンジンを削除 ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "エンジンを削除すると、元に戻せません。インデックスには影響しません。", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "このエンジンを完全に削除しますか?", @@ -13090,8 +13088,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "設定", "xpack.enterpriseSearch.nav.contentTitle": "コンテンツ", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "インデックス", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "スキーマ", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "概要", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "検索エクスペリエンス", "xpack.enterpriseSearch.nav.searchIndicesTitle": "インデックス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed0f6a5c2fedb..7199ea360f5e5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12229,7 +12229,6 @@ "xpack.enterpriseSearch.content.engine.indices.docsCount.columnTitle": "文档计数", "xpack.enterpriseSearch.content.engine.indices.health.columnTitle": "索引运行状况", "xpack.enterpriseSearch.content.engine.indices.name.columnTitle": "索引名称", - "xpack.enterpriseSearch.content.engine.indices.pageTitle": "索引", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description": "这不会删除该索引。您可以在稍后将其重新添加到此引擎。", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text": "是,移除此索引", "xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title": "从引擎中移除此索引", @@ -12237,7 +12236,6 @@ "xpack.enterpriseSearch.content.engine.indicesSelect.docsLabel": "文档:", "xpack.enterpriseSearch.content.engine.schema.field_name.columnTitle": "字段名称", "xpack.enterpriseSearch.content.engine.schema.field_type.columnTitle": "字段类型", - "xpack.enterpriseSearch.content.engine.schema.pageTitle": "架构", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.confirmButton.title": "是,删除此引擎 ", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.delete.description": "删除引擎是不可逆操作。您的索引不会受到影响。", "xpack.enterpriseSearch.content.engineList.deleteEngineModal.title": "永久删除此引擎?", @@ -13091,8 +13089,6 @@ "xpack.enterpriseSearch.nav.contentSettingsTitle": "设置", "xpack.enterpriseSearch.nav.contentTitle": "内容", "xpack.enterpriseSearch.nav.elasticsearchTitle": "Elasticsearch", - "xpack.enterpriseSearch.nav.engine.indicesTitle": "索引", - "xpack.enterpriseSearch.nav.engine.schemaTitle": "架构", "xpack.enterpriseSearch.nav.enterpriseSearchOverviewTitle": "概览", "xpack.enterpriseSearch.nav.searchExperiencesTitle": "搜索体验", "xpack.enterpriseSearch.nav.searchIndicesTitle": "索引", From 32de23bdb348bea70b31c7746632d20b6193bc77 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 24 Apr 2023 12:40:48 -0700 Subject: [PATCH 24/36] [Dashboard] Scroll to new panel (#152056) ## Summary Closes #97064. This scrolls to a newly added panel on a dashboard instead of remaining at the top. The user can see the new panel without having to manually scroll to the bottom. ~This also scrolls to the maximized panel when you minimize instead of just throwing you back to the top of the dashboard.~ Note: Scrolling on minimize will be addressed in a future PR. This scrolling behavior also seems to work with portable dashboards embedded in another apps, but it may require additional work on the consumer to call `scrollToPanel` in the appropriate callbacks when adding panels. #### Scrolls to newly added panel and shows a success border animation ![Apr-18-2023 07-40-41](https://user-images.githubusercontent.com/1697105/232812491-5bf3ee3a-c81d-4dd3-8b04-67978da3b9a8.gif) #### Scrolls to panel on return from editor ![Apr-18-2023 07-56-35](https://user-images.githubusercontent.com/1697105/232817401-6cfd7085-91b6-4f05-be1c-e47f6cc3edab.gif) #### Scrolls to panel clone ![Apr-18-2023 07-54-43](https://user-images.githubusercontent.com/1697105/232816928-2b473778-76e1-4781-8e51-f9e46ab74b9b.gif) #### Scrolling in portable dashboards example ![Apr-18-2023 08-13-14](https://user-images.githubusercontent.com/1697105/232822632-ffcbd9ad-9cad-4185-931c-a68fbf7e0fbe.gif) ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Hannah Mudge --- .../controls_example/public/edit_example.tsx | 2 +- .../editor/open_add_data_control_flyout.tsx | 36 +++++++++++++----- .../dashboard_actions/clone_panel_action.tsx | 1 + .../dashboard_actions/expand_panel_action.tsx | 4 ++ .../replace_panel_flyout.tsx | 2 + .../add_data_control_button.tsx | 8 +++- .../add_time_slider_control_button.tsx | 7 +++- .../top_nav/dashboard_editing_toolbar.tsx | 2 + .../component/grid/_dashboard_grid.scss | 10 +++-- .../component/grid/dashboard_grid.tsx | 13 ++++++- .../component/grid/dashboard_grid_item.tsx | 16 +++++++- .../component/panel/_dashboard_panel.scss | 25 +++++++++++++ .../panel/dashboard_panel_placement.ts | 1 + .../embeddable/api/add_panel_from_library.ts | 4 ++ .../embeddable/api/panel_management.ts | 11 ++++-- .../embeddable/create/create_dashboard.ts | 22 +++++++---- .../embeddable/dashboard_container.tsx | 37 +++++++++++++++++++ .../state/dashboard_container_reducers.ts | 8 ++++ .../public/dashboard_container/types.ts | 2 + .../add_panel/add_panel_flyout.tsx | 6 ++- .../add_panel/open_add_panel_flyout.tsx | 3 ++ 21 files changed, 188 insertions(+), 32 deletions(-) diff --git a/examples/controls_example/public/edit_example.tsx b/examples/controls_example/public/edit_example.tsx index f6297befa615c..148867337fedd 100644 --- a/examples/controls_example/public/edit_example.tsx +++ b/examples/controls_example/public/edit_example.tsx @@ -133,7 +133,7 @@ export const EditExample = () => { iconType="plusInCircle" isDisabled={controlGroupAPI === undefined} onClick={() => { - controlGroupAPI!.openAddDataControlFlyout(controlInputTransform); + controlGroupAPI!.openAddDataControlFlyout({ controlInputTransform }); }} > Add control diff --git a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx index bf60193432488..695eaa42e064d 100644 --- a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx +++ b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { ControlGroupContainer, @@ -32,8 +33,12 @@ import { DataControlInput, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '.. export function openAddDataControlFlyout( this: ControlGroupContainer, - controlInputTransform?: ControlInputTransform + options?: { + controlInputTransform?: ControlInputTransform; + onSave?: (id: string) => void; + } ) { + const { controlInputTransform, onSave } = options || {}; const { overlays: { openFlyout, openConfirm }, controls: { getControlFactory }, @@ -71,7 +76,7 @@ export function openAddDataControlFlyout( updateTitle={(newTitle) => (controlInput.title = newTitle)} updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })} updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })} - onSave={(type) => { + onSave={async (type) => { this.closeAllFlyouts(); if (!type) { return; @@ -86,17 +91,28 @@ export function openAddDataControlFlyout( controlInput = controlInputTransform({ ...controlInput }, type); } - if (type === OPTIONS_LIST_CONTROL) { - this.addOptionsListControl(controlInput as AddOptionsListControlProps); - return; - } + let newControl; - if (type === RANGE_SLIDER_CONTROL) { - this.addRangeSliderControl(controlInput as AddRangeSliderControlProps); - return; + switch (type) { + case OPTIONS_LIST_CONTROL: + newControl = await this.addOptionsListControl( + controlInput as AddOptionsListControlProps + ); + break; + case RANGE_SLIDER_CONTROL: + newControl = await this.addRangeSliderControl( + controlInput as AddRangeSliderControlProps + ); + break; + default: + newControl = await this.addDataControlFromField( + controlInput as AddDataControlProps + ); } - this.addDataControlFromField(controlInput as AddDataControlProps); + if (onSave && !isErrorEmbeddable(newControl)) { + onSave(newControl.id); + } }} onCancel={onCancel} onTypeEditorChange={(partialInput) => diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index 482553e2f002f..228db7138fd54 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -95,6 +95,7 @@ export class ClonePanelAction implements Action { height: panelToClone.gridData.h, currentPanels: dashboard.getInput().panels, placeBesideId: panelToClone.explicitInput.id, + scrollToPanel: true, } as IPanelPlacementBesideArgs ); } diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx index 0d3dd592dcc34..4e98a6dd31024 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx @@ -64,5 +64,9 @@ export class ExpandPanelAction implements Action { } const newValue = isExpanded(embeddable) ? undefined : embeddable.id; (embeddable.parent as DashboardContainer).setExpandedPanelId(newValue); + + if (!newValue) { + (embeddable.parent as DashboardContainer).setScrollToPanelId(embeddable.id); + } } } diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx index 4a2ac8f41d6a6..14067f0b6aa68 100644 --- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx @@ -21,6 +21,7 @@ import { Toast } from '@kbn/core/public'; import { DashboardPanelState } from '../../common'; import { pluginServices } from '../services/plugin_services'; import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings'; +import { DashboardContainer } from '../dashboard_container'; interface Props { container: IContainer; @@ -82,6 +83,7 @@ export class ReplacePanelFlyout extends React.Component { }, }); + (container as DashboardContainer).setHighlightPanelId(id); this.showToast(name); this.props.onClose(); }; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx index 6cef7e858b165..e7c7daa2bcc27 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { ControlGroupContainer } from '@kbn/controls-plugin/public'; import { getAddControlButtonTitle } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -17,6 +18,11 @@ interface Props { } export const AddDataControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { + const dashboard = useDashboardAPI(); + const onSave = () => { + dashboard.scrollToTop(); + }; + return ( { - controlGroup.openAddDataControlFlyout(); + controlGroup.openAddDataControlFlyout({ onSave }); closePopover(); }} > diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx index 8283144e1c155..cbd514be8ba13 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx @@ -13,6 +13,7 @@ import { getAddTimeSliderControlButtonTitle, getOnlyOneTimeSliderControlMsg, } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -21,6 +22,7 @@ interface Props { export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false); + const dashboard = useDashboardAPI(); useEffect(() => { const subscription = controlGroup.getInput$().subscribe(() => { @@ -42,8 +44,9 @@ export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest { - controlGroup.addTimeSliderControl(); + onClick={async () => { + await controlGroup.addTimeSliderControl(); + dashboard.scrollToTop(); closePopover(); }} data-test-subj="controls-create-timeslider-button" diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 03b609ae99736..708af176d785d 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -110,6 +110,8 @@ export function DashboardEditingToolbar() { const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput); if (newEmbeddable) { + dashboard.setScrollToPanelId(newEmbeddable.id); + dashboard.setHighlightPanelId(newEmbeddable.id); toasts.addSuccess({ title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), 'data-test-subj': 'addEmbeddableToDashboardSuccess', diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss index 7e9529a90be8b..cc96c816ce8b7 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss @@ -36,10 +36,13 @@ } /** - * When a single panel is expanded, all the other panels are hidden in the grid. + * When a single panel is expanded, all the other panels moved offscreen. + * Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize */ .dshDashboardGrid__item--hidden { - display: none; + position: absolute; + top: -9999px; + left: -9999px; } /** @@ -53,11 +56,12 @@ * 1. We need to mark this as important because react grid layout sets the width and height of the panels inline. */ .dshDashboardGrid__item--expanded { + position: absolute; height: 100% !important; /* 1 */ width: 100% !important; /* 1 */ top: 0 !important; /* 1 */ left: 0 !important; /* 1 */ - transform: translate(0, 0) !important; /* 1 */ + transform: none !important; padding: $euiSizeS; // Altered panel styles can be found in ../panel diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index b840fcd408977..0055e24685b89 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -12,7 +12,7 @@ import 'react-grid-layout/css/styles.css'; import { pick } from 'lodash'; import classNames from 'classnames'; import { useEffectOnce } from 'react-use/lib'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -38,6 +38,15 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { setTimeout(() => setAnimatePanelTransforms(true), 500); }); + useEffect(() => { + if (expandedPanelId) { + setAnimatePanelTransforms(false); + } else { + // delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize + setTimeout(() => setAnimatePanelTransforms(true), 0); + } + }, [expandedPanelId]); + const { onPanelStatusChange } = useDashboardPerformanceTracker({ panelCount: Object.keys(panels).length, }); @@ -98,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { 'dshLayout-withoutMargins': !useMargins, 'dshLayout--viewing': viewMode === ViewMode.VIEW, 'dshLayout--editing': viewMode !== ViewMode.VIEW, - 'dshLayout--noAnimation': !animatePanelTransforms, + 'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId, 'dshLayout-isMaximizedPanel': expandedPanelId !== undefined, }); diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx index 45aa70fd50feb..39ff6ebc48418 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; @@ -56,6 +56,8 @@ const Item = React.forwardRef( embeddable: { EmbeddablePanel: PanelComponent }, } = pluginServices.getServices(); const container = useDashboardContainer(); + const scrollToPanelId = container.select((state) => state.componentState.scrollToPanelId); + const highlightPanelId = container.select((state) => state.componentState.highlightPanelId); const expandPanel = expandedPanelId !== undefined && expandedPanelId === id; const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id; @@ -66,11 +68,23 @@ const Item = React.forwardRef( printViewport__vis: container.getInput().viewMode === ViewMode.PRINT, }); + useLayoutEffect(() => { + if (typeof ref !== 'function' && ref?.current) { + if (scrollToPanelId === id) { + container.scrollToPanel(ref.current); + } + if (highlightPanelId === id) { + container.highlightPanel(ref.current); + } + } + }, [id, container, scrollToPanelId, highlightPanelId, ref]); + return (
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss index f04e5e29d960b..f8715220ddf37 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss @@ -11,6 +11,10 @@ box-shadow: none; border-radius: 0; } + + .dshDashboardGrid__item--highlighted { + border-radius: 0; + } } // Remove border color unless in editing mode @@ -25,3 +29,24 @@ cursor: default; } } + +@keyframes highlightOutline { + 0% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } + 25% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, .5); + } + 100% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } +} + +.dshDashboardGrid__item--highlighted { + border-radius: $euiSizeXS; + animation-name: highlightOutline; + animation-duration: 4s; + animation-timing-function: ease-out; + // keeps outline from getting cut off by other panels without margins + z-index: 999 !important; +} diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts index 77b51874319ba..e570e1eadd6ca 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts @@ -24,6 +24,7 @@ export interface IPanelPlacementArgs { width: number; height: number; currentPanels: { [key: string]: DashboardPanelState }; + scrollToPanel?: boolean; } export interface IPanelPlacementBesideArgs extends IPanelPlacementArgs { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts index ef4f4dc7ea5c9..c708937e3d56e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts @@ -41,6 +41,10 @@ export function addFromLibrary(this: DashboardContainer) { notifications, overlays, theme, + onAddPanel: (id: string) => { + this.setScrollToPanelId(id); + this.setHighlightPanelId(id); + }, }) ); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts index cb2ce9af37bcd..7b02001a93c6c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts @@ -128,7 +128,12 @@ export function showPlaceholderUntil newStateComplete) - .then((newPanelState: Partial) => - this.replacePanel(placeholderPanelState, newPanelState) - ); + .then(async (newPanelState: Partial) => { + const panelId = await this.replacePanel(placeholderPanelState, newPanelState); + + if (placementArgs?.scrollToPanel) { + this.setScrollToPanelId(panelId); + this.setHighlightPanelId(panelId); + } + }); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 7609a4f3eb95f..f0a20e832e431 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -181,12 +181,13 @@ export const createDashboard = async ( const incomingEmbeddable = creationOptions?.incomingEmbeddable; if (incomingEmbeddable) { initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable. - if ( + + const panelExists = incomingEmbeddable.embeddableId && - Boolean(initialInput.panels[incomingEmbeddable.embeddableId]) - ) { + Boolean(initialInput.panels[incomingEmbeddable.embeddableId]); + if (panelExists) { // this embeddable already exists, we will update the explicit input. - const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId]; + const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId as string]; const sameType = panelToUpdate.type === incomingEmbeddable.type; panelToUpdate.type = incomingEmbeddable.type; @@ -195,17 +196,22 @@ export const createDashboard = async ( ...(sameType ? panelToUpdate.explicitInput : {}), ...incomingEmbeddable.input, - id: incomingEmbeddable.embeddableId, + id: incomingEmbeddable.embeddableId as string, // maintain hide panel titles setting. hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles, }; } else { // otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created. - untilDashboardReady().then((container) => - container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input) - ); + untilDashboardReady().then(async (container) => { + container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input); + }); } + + untilDashboardReady().then(async (container) => { + container.setScrollToPanelId(incomingEmbeddable.embeddableId); + container.setHighlightPanelId(incomingEmbeddable.embeddableId); + }); } // -------------------------------------------------------------------------------------- diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 5b7a589afa950..d5a5385e779b3 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -398,4 +398,41 @@ export class DashboardContainer extends Container { + this.dispatch.setScrollToPanelId(id); + }; + + public scrollToPanel = async (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.scrollToPanelId; + if (!id) return; + + this.untilEmbeddableLoaded(id).then(() => { + this.setScrollToPanelId(undefined); + panelRef.scrollIntoView({ block: 'center' }); + }); + }; + + public scrollToTop = () => { + window.scroll(0, 0); + }; + + public setHighlightPanelId = (id: string | undefined) => { + this.dispatch.setHighlightPanelId(id); + }; + + public highlightPanel = (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.highlightPanelId; + + if (id && panelRef) { + this.untilEmbeddableLoaded(id).then(() => { + panelRef.classList.add('dshDashboardGrid__item--highlighted'); + // Removes the class after the highlight animation finishes + setTimeout(() => { + panelRef.classList.remove('dshDashboardGrid__item--highlighted'); + }, 5000); + }); + } + this.setHighlightPanelId(undefined); + }; } diff --git a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts index 70bf3a7d65989..86a58bb72f639 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts @@ -209,4 +209,12 @@ export const dashboardContainerReducers = { setHasOverlays: (state: DashboardReduxState, action: PayloadAction) => { state.componentState.hasOverlays = action.payload; }, + + setScrollToPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.scrollToPanelId = action.payload; + }, + + setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.highlightPanelId = action.payload; + }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 6e8ff1f5c98a0..544317d9f6bcc 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -33,6 +33,8 @@ export interface DashboardPublicState { fullScreenMode?: boolean; savedQueryId?: string; lastSavedId?: string; + scrollToPanelId?: string; + highlightPanelId?: string; } export interface DashboardRenderPerformanceStats { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index dcaa3880678ab..ea7c150bf38b8 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -30,6 +30,7 @@ interface Props { SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; + onAddPanel?: (id: string) => void; } interface State { @@ -101,7 +102,7 @@ export class AddPanelFlyout extends React.Component { throw new EmbeddableFactoryNotFoundError(savedObjectType); } - this.props.container.addNewEmbeddable( + const embeddable = await this.props.container.addNewEmbeddable( factoryForSavedObjectType.type, { savedObjectId } ); @@ -109,6 +110,9 @@ export class AddPanelFlyout extends React.Component { this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so); this.showToast(name); + if (this.props.onAddPanel) { + this.props.onAddPanel(embeddable.id); + } }; private doTelemetryForAddEvent( diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 4cc5a7ccb6e11..eb2722dcf9869 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -24,6 +24,7 @@ export function openAddPanelFlyout(options: { showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; theme: ThemeServiceStart; + onAddPanel?: (id: string) => void; }): OverlayRef { const { embeddable, @@ -35,11 +36,13 @@ export function openAddPanelFlyout(options: { showCreateNewMenu, reportUiCounter, theme, + onAddPanel, } = options; const flyoutSession = overlays.openFlyout( toMountPoint( { if (flyoutSession) { flyoutSession.close(); From 275c36031428d7ec2881e0c57154d1b3a96ade5f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 24 Apr 2023 16:19:59 -0400 Subject: [PATCH 25/36] [Synthetics] enable auto re-generation of monitor management api when read permissions are missing (#155203) Resolves https://github.com/elastic/kibana/issues/151695 Auto regenerates the synthetics api key when it does not include `synthetics-*` read permissions. Also ensures key are regenerated when deleted via stack management. A user without permissions to enable monitor management will see this callout when monitor management is disabled for either reason ![Synthetics-Overview-Synthetics-Kibana (1)](https://user-images.githubusercontent.com/11356435/232926046-ea39115b-acc7-40a7-8ec1-de77a20daf53.png) ## Testing lack of read permissions This PR is hard to test. I did so by adjusting the code to force the creation of an api key without read permissions. Here's how I did it: 1. connect to a clean ES instance by creating a new oblt cluster or running `yarn es snapshot 2. Remove read permissions for the api key https://github.com/elastic/kibana/pull/155203/files#diff-e38e55402aedfdb1a8a17bdd557364cd3649e1590b5e92fb44ed639f03ba880dR30 3. Remove read permission check here https://github.com/elastic/kibana/pull/155203/files#diff-e38e55402aedfdb1a8a17bdd557364cd3649e1590b5e92fb44ed639f03ba880dR60 4. Navigate to Synthetics app and create your first monitor 5. Navigate to Stack Management -> Api Keys. Click on he api key to inspect it's privileges. You should not see `read` permissions. 6. Remove the changes listed in step 2 and 3 and make sure the branch is back in sync with this PR 7. Navigate to the Synthetics app again. 9. Navigate to stack management -> api keys. Ensure there is only one synthetics monitor management api key. Click on he api key to inspect it's privileges. You should now see `read` permissions. 10. Delete this api key 11. Navigate back to the Synthetics app 12. Navigate back to stack management -> api keys. Notice tha api key has been regenerated --- .../management/disabled_callout.tsx | 40 +-- .../management/invalid_api_key_callout.tsx | 80 ----- .../monitors_page/management/labels.ts | 6 +- .../synthetics_enablement.tsx | 40 +-- .../monitors_page/overview/overview_page.tsx | 2 +- .../apps/synthetics/hooks/use_enablement.ts | 11 +- .../state/synthetics_enablement/actions.ts | 14 - .../state/synthetics_enablement/api.ts | 10 +- .../state/synthetics_enablement/effects.ts | 27 +- .../state/synthetics_enablement/index.ts | 39 -- .../lib/saved_objects/service_api_key.ts | 2 +- .../plugins/synthetics/server/routes/index.ts | 2 - .../routes/synthetics_service/enablement.ts | 56 +-- .../synthetics_service/get_api_key.test.ts | 63 +++- .../server/synthetics_service/get_api_key.ts | 26 +- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../apis/synthetics/add_monitor_project.ts | 2 +- .../apis/synthetics/edit_monitor.ts | 2 +- .../apis/synthetics/get_monitor.ts | 2 +- .../apis/synthetics/get_monitor_overview.ts | 2 +- .../apis/synthetics/synthetics_enablement.ts | 334 ++++++++++++++---- 23 files changed, 395 insertions(+), 380 deletions(-) delete mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx index 0a75b9a44499b..885f44eaa5ae1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx @@ -6,41 +6,24 @@ */ import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; -import { InvalidApiKeyCalloutCallout } from './invalid_api_key_callout'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import * as labels from './labels'; import { useEnablement } from '../../../hooks'; export const DisabledCallout = ({ total }: { total: number }) => { - const { enablement, enableSynthetics, invalidApiKeyError, loading } = useEnablement(); + const { enablement, invalidApiKeyError, loading } = useEnablement(); const showDisableCallout = !enablement.isEnabled && total > 0; - const showInvalidApiKeyError = invalidApiKeyError && total > 0; + const showInvalidApiKeyCallout = invalidApiKeyError && total > 0; - if (showInvalidApiKeyError) { - return ; - } - - if (!showDisableCallout) { + if (!showDisableCallout && !showInvalidApiKeyCallout) { return null; } - return ( - -

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {labels.SYNTHETICS_ENABLE_LABEL} - - ) : ( + return !enablement.canEnable && !loading ? ( + <> + +

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} { {labels.LEARN_MORE_LABEL}

- )} -
- ); +
+ + + ) : null; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx deleted file mode 100644 index 70816a69c2188..0000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 React from 'react'; -import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useEnablement } from '../../../hooks'; - -export const InvalidApiKeyCalloutCallout = () => { - const { enablement, enableSynthetics, loading } = useEnablement(); - - return ( - <> - -

{CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {SYNTHETICS_ENABLE_LABEL} - - ) : ( -

- {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - - {LEARN_MORE_LABEL} - -

- )} -
- - - ); -}; - -const LEARN_MORE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey', - { - defaultMessage: 'Learn more', - } -); - -const API_KEY_MISSING = i18n.translate('xpack.synthetics.monitorManagement.callout.apiKeyMissing', { - defaultMessage: 'Monitor Management is currently disabled because of missing API key', -}); - -const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey', - { - defaultMessage: 'Contact your administrator to enable Monitor Management.', - } -); - -const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.description.invalidKey', - { - defaultMessage: `Monitor Management is currently disabled. To run your monitors in one of Elastic's global managed testing locations, you need to re-enable monitor management.`, - } -); - -const SYNTHETICS_ENABLE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey', - { - defaultMessage: 'Enable monitor management', - } -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts index aca280e74fcb2..ff297267dcb62 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts @@ -24,14 +24,14 @@ export const LEARN_MORE_LABEL = i18n.translate( export const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled', { - defaultMessage: 'Monitor Management is disabled', + defaultMessage: 'Monitor Management is currently disabled', } ); export const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled.adminContact', { - defaultMessage: 'Please contact your administrator to enable Monitor Management.', + defaultMessage: 'Monitor Management will be enabled when an admin visits the Synthetics app.', } ); @@ -39,7 +39,7 @@ export const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( 'xpack.synthetics.monitorManagement.callout.description.disabled', { defaultMessage: - 'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.', + "Monitor Management requires a valid API key to run your monitors on Elastic's global managed testing locations. If you already had enabled Monitor Management previously, the API key may no longer be valid.", } ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx index d6b927bbc43b3..e4fcee12f65a5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx @@ -6,16 +6,16 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiTitle, EuiLink } from '@elastic/eui'; import { useEnablement } from '../../../../hooks/use_enablement'; import { kibanaService } from '../../../../../../utils/kibana_service'; import * as labels from './labels'; export const EnablementEmptyState = () => { - const { error, enablement, enableSynthetics, loading } = useEnablement(); + const { error, enablement, loading } = useEnablement(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const [isEnabling, setIsEnabling] = useState(false); - const { isEnabled, canEnable } = enablement; + const { isEnabled } = enablement; const isEnabledRef = useRef(isEnabled); const buttonRef = useRef(null); @@ -44,11 +44,6 @@ export const EnablementEmptyState = () => { } }, [isEnabled, isEnabling, error]); - const handleEnableSynthetics = () => { - enableSynthetics(); - setIsEnabling(true); - }; - useEffect(() => { if (shouldFocusEnablementButton) { buttonRef.current?.focus(); @@ -57,33 +52,8 @@ export const EnablementEmptyState = () => { return !isEnabled && !loading ? ( - {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_LABEL - : labels.SYNTHETICS_APP_DISABLED_LABEL} - - } - body={ -

- {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE - : labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE} -

- } - actions={ - canEnable ? ( - - {labels.MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL} - - ) : null - } + title={

{labels.SYNTHETICS_APP_DISABLED_LABEL}

} + body={

{labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE}

} footer={ <> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index a98f78249adbe..2e4eb6ca03a31 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -101,8 +101,8 @@ export const OverviewPage: React.FC = () => { return ( <> - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts index 394da8aefc086..fe726d0cbe3d2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts @@ -5,14 +5,9 @@ * 2.0. */ -import { useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - getSyntheticsEnablement, - enableSynthetics, - disableSynthetics, - selectSyntheticsEnablement, -} from '../state'; +import { getSyntheticsEnablement, selectSyntheticsEnablement } from '../state'; export function useEnablement() { const dispatch = useDispatch(); @@ -35,7 +30,5 @@ export function useEnablement() { invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false, error, loading, - enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), - disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]), }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts index 7369ce0917e5a..78c0d9484149e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts @@ -16,17 +16,3 @@ export const getSyntheticsEnablementSuccess = createAction( '[SYNTHETICS_ENABLEMENT] GET FAILURE' ); - -export const disableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] DISABLE'); -export const disableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] DISABLE SUCCESS'); -export const disableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] DISABLE FAILURE' -); - -export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE'); -export const enableSyntheticsSuccess = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS' -); -export const enableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE FAILURE' -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts index 62b48676e3965..2e009cc0b89d2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts @@ -14,17 +14,9 @@ import { apiService } from '../../../../utils/api_service'; export const fetchGetSyntheticsEnablement = async (): Promise => { - return await apiService.get( + return await apiService.put( API_URLS.SYNTHETICS_ENABLEMENT, undefined, MonitorManagementEnablementResultCodec ); }; - -export const fetchDisableSynthetics = async (): Promise<{}> => { - return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); -}; - -export const fetchEnableSynthetics = async (): Promise => { - return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts index d3134c60f8fd3..14c912b07ce99 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts @@ -5,20 +5,15 @@ * 2.0. */ -import { takeLatest, takeLeading } from 'redux-saga/effects'; +import { takeLeading } from 'redux-saga/effects'; +import { i18n } from '@kbn/i18n'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, getSyntheticsEnablementFailure, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, } from './actions'; -import { fetchGetSyntheticsEnablement, fetchDisableSynthetics, fetchEnableSynthetics } from './api'; import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchGetSyntheticsEnablement } from './api'; export function* fetchSyntheticsEnablementEffect() { yield takeLeading( @@ -26,15 +21,13 @@ export function* fetchSyntheticsEnablementEffect() { fetchEffectFactory( fetchGetSyntheticsEnablement, getSyntheticsEnablementSuccess, - getSyntheticsEnablementFailure + getSyntheticsEnablementFailure, + undefined, + failureMessage ) ); - yield takeLatest( - disableSynthetics, - fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure) - ); - yield takeLatest( - enableSynthetics, - fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure) - ); } + +const failureMessage = i18n.translate('xpack.synthetics.settings.enablement.fail', { + defaultMessage: 'Failed to enable Monitor Management', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts index 62cbce9bfe05b..26bf2b50b8325 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts @@ -9,12 +9,6 @@ import { createReducer } from '@reduxjs/toolkit'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, getSyntheticsEnablementFailure, } from './actions'; import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types'; @@ -45,39 +39,6 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder) .addCase(getSyntheticsEnablementFailure, (state, action) => { state.loading = false; state.error = action.payload; - }) - - .addCase(disableSynthetics, (state) => { - state.loading = true; - }) - .addCase(disableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = { - canEnable: state.enablement?.canEnable ?? false, - areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, - canManageApiKeys: state.enablement?.canManageApiKeys ?? false, - isEnabled: false, - isValidApiKey: true, - }; - }) - .addCase(disableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; - }) - - .addCase(enableSynthetics, (state) => { - state.loading = true; - state.enablement = null; - }) - .addCase(enableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = action.payload; - }) - .addCase(enableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; }); }); diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts index adab53c9d4268..3c62f99f7e67b 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts @@ -72,7 +72,7 @@ const getSyntheticsServiceAPIKey = async (server: UptimeServerSetup) => { } }; -const setSyntheticsServiceApiKey = async ( +export const setSyntheticsServiceApiKey = async ( soClient: SavedObjectsClientContract, apiKey: SyntheticsServiceApiKey ) => { diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 9e2038d05962a..836143d55f014 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -20,7 +20,6 @@ import { getServiceLocationsRoute } from './synthetics_service/get_service_locat import { deleteSyntheticsMonitorRoute } from './monitor_cruds/delete_monitor'; import { disableSyntheticsRoute, - enableSyntheticsRoute, getSyntheticsEnablementRoute, } from './synthetics_service/enablement'; import { @@ -61,7 +60,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ deleteSyntheticsMonitorProjectRoute, disableSyntheticsRoute, editSyntheticsMonitorRoute, - enableSyntheticsRoute, getServiceLocationsRoute, getSyntheticsMonitorRoute, getSyntheticsProjectMonitorsRoute, diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts index c4561f3ee9e00..87a10dbee9a8e 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts @@ -5,18 +5,12 @@ * 2.0. */ import { syntheticsServiceAPIKeySavedObject } from '../../legacy_uptime/lib/saved_objects/service_api_key'; -import { - SyntheticsRestApiRouteFactory, - UMRestApiRouteFactory, -} from '../../legacy_uptime/routes/types'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; -import { - generateAndSaveServiceAPIKey, - SyntheticsForbiddenError, -} from '../../synthetics_service/get_api_key'; +import { generateAndSaveServiceAPIKey } from '../../synthetics_service/get_api_key'; -export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'GET', +export const getSyntheticsEnablementRoute: SyntheticsRestApiRouteFactory = (libs) => ({ + method: 'PUT', path: API_URLS.SYNTHETICS_ENABLEMENT, validate: {}, handler: async ({ savedObjectsClient, request, server }): Promise => { @@ -25,7 +19,18 @@ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ server, }); const { canEnable, isEnabled } = result; - if (canEnable && !isEnabled && server.config.service?.manifestUrl) { + const { security } = server; + const { apiKey, isValid } = await libs.requests.getAPIKeyForSyntheticsService({ + server, + }); + if (apiKey && !isValid) { + await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); + await security.authc.apiKeys?.invalidateAsInternalUser({ + ids: [apiKey?.id || ''], + }); + } + const regenerationRequired = !isEnabled || !isValid; + if (canEnable && regenerationRequired && server.config.service?.manifestUrl) { await generateAndSaveServiceAPIKey({ request, authSavedObjectsClient: savedObjectsClient, @@ -68,7 +73,7 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( server, }); await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); - await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); + await security.authc.apiKeys?.invalidateAsInternalUser({ ids: [apiKey?.id || ''] }); return response.ok({}); } catch (e) { server.logger.error(e); @@ -76,30 +81,3 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( } }, }); - -export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'POST', - path: API_URLS.SYNTHETICS_ENABLEMENT, - validate: {}, - handler: async ({ request, response, server, savedObjectsClient }): Promise => { - const { logger } = server; - try { - await generateAndSaveServiceAPIKey({ - request, - authSavedObjectsClient: savedObjectsClient, - server, - }); - return response.ok({ - body: await libs.requests.getSyntheticsEnablement({ - server, - }), - }); - } catch (e) { - logger.error(e); - if (e instanceof SyntheticsForbiddenError) { - return response.forbidden(); - } - throw e; - } - }, -}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts index 4b15f4da43515..ca4a18e88d5d9 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts @@ -25,16 +25,6 @@ describe('getAPIKeyTest', function () { const logger = loggerMock.create(); - jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ - index: { - [syntheticsIndex]: { - auto_configure: true, - create_doc: true, - view_index_metadata: true, - }, - }, - } as any); - const server = { logger, security, @@ -52,6 +42,20 @@ describe('getAPIKeyTest', function () { encoded: '@#$%^&', }); + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: true, + }, + }, + } as any); + }); + it('should return existing api key', async () => { const getObject = jest .fn() @@ -79,4 +83,43 @@ describe('getAPIKeyTest', function () { 'ba997842-b0cf-4429-aa9d-578d9bf0d391' ); }); + + it('invalidates api keys with missing read permissions', async () => { + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: false, + }, + }, + } as any); + + const getObject = jest + .fn() + .mockReturnValue({ attributes: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' } }); + + encryptedSavedObjects.getClient = jest.fn().mockReturnValue({ + getDecryptedAsInternalUser: getObject, + }); + const apiKey = await getAPIKeyForSyntheticsService({ + server, + }); + + expect(apiKey).toEqual({ + apiKey: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' }, + isValid: false, + }); + + expect(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1); + expect(getObject).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjects.getClient).toHaveBeenCalledWith({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + expect(getObject).toHaveBeenCalledWith( + 'uptime-synthetics-api-key', + 'ba997842-b0cf-4429-aa9d-578d9bf0d391' + ); + }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts index 0bc4f656901f4..79af4d6cfc718 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts @@ -56,7 +56,8 @@ export const getAPIKeyForSyntheticsService = async ({ const hasPermissions = indexPermissions.auto_configure && indexPermissions.create_doc && - indexPermissions.view_index_metadata; + indexPermissions.view_index_metadata && + indexPermissions.read; if (!hasPermissions) { return { isValid: false, apiKey }; @@ -92,6 +93,7 @@ export const generateAPIKey = async ({ } if (uptimePrivileges) { + /* Exposed to the user. Must create directly with the user */ return security.authc.apiKeys?.create(request, { name: 'synthetics-api-key (required for project monitors)', kibana_role_descriptors: { @@ -122,7 +124,8 @@ export const generateAPIKey = async ({ throw new SyntheticsForbiddenError(); } - return security.authc.apiKeys?.create(request, { + /* Not exposed to the user. May grant as internal user */ + return security.authc.apiKeys?.grantAsInternalUser(request, { name: 'synthetics-api-key (required for monitor management)', role_descriptors: { synthetics_writer: serviceApiKeyPrivileges, @@ -160,23 +163,24 @@ export const generateAndSaveServiceAPIKey = async ({ export const getSyntheticsEnablement = async ({ server }: { server: UptimeServerSetup }) => { const { security, config } = server; + const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ + getAPIKeyForSyntheticsService({ server }), + hasEnablePermissions(server), + security.authc.apiKeys.areAPIKeysEnabled(), + ]); + + const { canEnable, canManageApiKeys } = hasPrivileges; + if (!config.service?.manifestUrl) { return { canEnable: true, - canManageApiKeys: true, + canManageApiKeys, isEnabled: true, isValidApiKey: true, areApiKeysEnabled: true, }; } - const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ - getAPIKeyForSyntheticsService({ server }), - hasEnablePermissions(server), - security.authc.apiKeys.areAPIKeysEnabled(), - ]); - - const { canEnable, canManageApiKeys } = hasPrivileges; return { canEnable, canManageApiKeys, @@ -217,7 +221,7 @@ const hasEnablePermissions = async ({ uptimeEsClient }: UptimeServerSetup) => { return { canManageApiKeys, - canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, + canEnable: hasClusterPermissions && hasIndexPermissions, }; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f7cf6a952a796..fda996fca0a65 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -34922,12 +34922,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "Clé d'API", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "Cette clé d’API ne sera affichée qu'une seule fois. Veuillez en conserver une copie pour vos propres dossiers.", "xpack.synthetics.monitorManagement.areYouSure": "Voulez-vous vraiment supprimer cet emplacement ?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "La Gestion des moniteurs est actuellement désactivée en raison d'une clé d'API manquante", "xpack.synthetics.monitorManagement.callout.description.disabled": "La Gestion des moniteurs est actuellement désactivée. Pour exécuter vos moniteurs sur le service Synthetics géré par Elastic, activez la Gestion des moniteurs. Vos moniteurs existants ont été suspendus.", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "La Gestion des moniteurs est actuellement désactivée. Pour exécuter vos moniteurs dans l'un des emplacements de tests gérés globaux d'Elastic, vous devez ré-activer la Gestion des moniteurs.", "xpack.synthetics.monitorManagement.callout.disabled": "La Gestion des moniteurs est désactivée", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "Veuillez contacter votre administrateur pour activer la Gestion des moniteurs.", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "Contactez votre administrateur pour activer la Gestion des moniteurs.", "xpack.synthetics.monitorManagement.cancelLabel": "Annuler", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "Vous n'êtes pas autorisé à mettre à jour les intégrations. Des autorisations d'écriture pour les intégrations sont requises.", "xpack.synthetics.monitorManagement.closeButtonLabel": "Fermer", @@ -34976,7 +34973,6 @@ "xpack.synthetics.monitorManagement.locationName": "Nom de l’emplacement", "xpack.synthetics.monitorManagement.locationsLabel": "Emplacements", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "Chargement de la liste Gestion des moniteurs", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "En savoir plus", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "En savoir plus.", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "Moniteur ajouté avec succès.", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "Moniteur mis à jour.", @@ -35017,7 +35013,6 @@ "xpack.synthetics.monitorManagement.steps": "Étapes", "xpack.synthetics.monitorManagement.summary.heading": "Résumé", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "Gestion des moniteurs désactivée avec succès.", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "Activer la Gestion des moniteurs", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "Activer la Gestion des moniteurs", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "Gestion des moniteurs activée avec succès.", "xpack.synthetics.monitorManagement.testResult": "Résultat du test", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2e5fdf22d200b..11ccd44a6eee7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -34901,12 +34901,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "API キー", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "このAPIキーは1回だけ表示されます。自分の記録用にコピーして保管してください。", "xpack.synthetics.monitorManagement.areYouSure": "この場所を削除しますか?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "現在、APIキーがないため、モニター管理は無効です", "xpack.synthetics.monitorManagement.callout.description.disabled": "モニター管理は現在無効です。Elasticで管理されたSyntheticsサービスでモニターを実行するには、モニター管理を有効にします。既存のモニターが一時停止しています。", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "モニター管理は現在無効です。Elasticのグローバル管理されたテストロケーションのいずれかでモニターを実行するには、モニター管理を再有効化する必要があります。", "xpack.synthetics.monitorManagement.callout.disabled": "モニター管理が無効です", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "モニター管理を有効にするには、管理者に連絡してください。", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "モニター管理を有効にするには、管理者に連絡してください。", "xpack.synthetics.monitorManagement.cancelLabel": "キャンセル", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "統合を更新する権限がありません。統合書き込み権限が必要です。", "xpack.synthetics.monitorManagement.closeButtonLabel": "閉じる", @@ -34955,7 +34952,6 @@ "xpack.synthetics.monitorManagement.locationName": "場所名", "xpack.synthetics.monitorManagement.locationsLabel": "場所", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "モニター管理を読み込んでいます", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "詳細", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "詳細情報", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "モニターが正常に追加されました。", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "モニターは正常に更新されました。", @@ -34996,7 +34992,6 @@ "xpack.synthetics.monitorManagement.steps": "ステップ", "xpack.synthetics.monitorManagement.summary.heading": "まとめ", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "モニター管理は正常に無効にされました。", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "モニター管理を有効にする", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "モニター管理を有効にする", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "モニター管理は正常に有効にされました。", "xpack.synthetics.monitorManagement.testResult": "テスト結果", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7199ea360f5e5..4eae043b7b884 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -34917,12 +34917,9 @@ "xpack.synthetics.monitorManagement.apiKey.label": "API 密钥", "xpack.synthetics.monitorManagement.apiKeyWarning.label": "此 API 密钥仅显示一次。请保留副本作为您自己的记录。", "xpack.synthetics.monitorManagement.areYouSure": "是否确定要删除此位置?", - "xpack.synthetics.monitorManagement.callout.apiKeyMissing": "由于缺少 API 密钥,监测管理当前已禁用", "xpack.synthetics.monitorManagement.callout.description.disabled": "监测管理当前处于禁用状态。要在 Elastic 托管 Synthetics 服务上运行监测,请启用监测管理。现有监测已暂停。", - "xpack.synthetics.monitorManagement.callout.description.invalidKey": "监测管理当前处于禁用状态。要在 Elastic 的全球托管测试位置之一运行监测,您需要重新启用监测管理。", "xpack.synthetics.monitorManagement.callout.disabled": "已禁用监测管理", "xpack.synthetics.monitorManagement.callout.disabled.adminContact": "请联系管理员启用监测管理。", - "xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey": "请联系管理员启用监测管理。", "xpack.synthetics.monitorManagement.cancelLabel": "取消", "xpack.synthetics.monitorManagement.cannotSaveIntegration": "您无权更新集成。需要集成写入权限。", "xpack.synthetics.monitorManagement.closeButtonLabel": "关闭", @@ -34971,7 +34968,6 @@ "xpack.synthetics.monitorManagement.locationName": "位置名称", "xpack.synthetics.monitorManagement.locationsLabel": "位置", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel": "正在加载监测管理", - "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey": "了解详情", "xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore": "了解详情。", "xpack.synthetics.monitorManagement.monitorAddedSuccessMessage": "已成功添加监测。", "xpack.synthetics.monitorManagement.monitorEditedSuccessMessage": "已成功更新监测。", @@ -35012,7 +35008,6 @@ "xpack.synthetics.monitorManagement.steps": "步长", "xpack.synthetics.monitorManagement.summary.heading": "摘要", "xpack.synthetics.monitorManagement.syntheticsDisabledSuccess": "已成功禁用监测管理。", - "xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey": "启用监测管理", "xpack.synthetics.monitorManagement.syntheticsEnableLabel.management": "启用监测管理", "xpack.synthetics.monitorManagement.syntheticsEnableSuccess": "已成功启用监测管理。", "xpack.synthetics.monitorManagement.testResult": "测试结果", diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index b1bc58abe7113..e6a8ae14cda1a 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertest .post('/api/fleet/epm/packages/synthetics/0.11.4') diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index f20a2cdf61a45..bf4447a1b5969 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { _httpMonitorJson = getFixtureJson('http_monitor'); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); const testPolicyName = 'Fleet test server policy' + Date.now(); const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts index 5394ca64545e6..00772c5550ac1 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts @@ -32,7 +32,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); _monitors = [ getFixtureJson('icmp_monitor'), diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts index 25aad0704cddd..625dbdac61608 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts @@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); await security.role.create(roleName, { kibana: [ diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index d031a6c505c8f..bf68da4c148f5 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -6,24 +6,57 @@ */ import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + syntheticsApiKeyID, + syntheticsApiKeyObjectType, +} from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/service_api_key'; import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { + const correctPrivileges = { + applications: [], + cluster: ['monitor', 'read_ilm', 'read_pipeline'], + indices: [ + { + allow_restricted_indices: false, + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure', 'read'], + }, + ], + metadata: {}, + run_as: [], + transient_metadata: { + enabled: true, + }, + }; + describe('SyntheticsEnablement', () => { const supertestWithAuth = getService('supertest'); const supertest = getService('supertestWithoutAuth'); const security = getService('security'); const kibanaServer = getService('kibanaServer'); - before(async () => { - await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); - }); - - describe('[GET] - /internal/uptime/service/enablement', () => { - ['manage_security', 'manage_own_api_key', 'manage_api_key'].forEach((privilege) => { - it(`returns response for an admin with privilege ${privilege}`, async () => { + const esSupertest = getService('esSupertest'); + + const getApiKeys = async () => { + const { body } = await esSupertest.get(`/_security/api_key`).query({ with_limited_by: true }); + const apiKeys = body.api_keys || []; + return apiKeys.filter( + (apiKey: any) => apiKey.name.includes('synthetics-api-key') && apiKey.invalidated === false + ); + }; + + describe('[PUT] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); + ['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { + it(`returns response when user can manage api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin-${privilege}`; const password = `${username}-password`; @@ -38,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: [privilege, ...serviceApiKeyPrivileges.cluster], + cluster: [privilege], indices: serviceApiKeyPrivileges.indices, }, }); @@ -50,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -58,17 +91,10 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: true, - canEnable: true, - isEnabled: true, - isValidApiKey: true, + canEnable: false, + isEnabled: false, + isValidApiKey: false, }); - if (privilege !== 'manage_own_api_key') { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(200); - } } finally { await security.user.delete(username); await security.role.delete(roleName); @@ -76,9 +102,9 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('returns response for an uptime all user without admin privileges', async () => { - const username = 'uptime'; - const roleName = 'uptime_user'; + it(`returns response for an admin with privilege`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -90,7 +116,10 @@ export default function ({ getService }: FtrProviderContext) { spaces: ['*'], }, ], - elasticsearch: {}, + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, }); await security.user.create(username, { @@ -100,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -108,19 +137,20 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, - canEnable: false, - isEnabled: false, - isValidApiKey: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, }); + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await security.role.delete(roleName); await security.user.delete(username); + await security.role.delete(roleName); } }); - }); - describe('[POST] - /internal/uptime/service/enablement', () => { - it('with an admin', async () => { + it(`does not create excess api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin`; const password = `${username}-password`; @@ -135,7 +165,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -146,38 +176,213 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables the api key when created with invalid permissions and invalidates old api key`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + // create api key with incorrect permissions + const apiKeyResult = await esSupertest + .post(`/_security/api_key`) + .send({ + name: 'synthetics-api-key', + expiration: '1d', + role_descriptors: { + 'role-a': { + cluster: serviceApiKeyPrivileges.cluster, + indices: [ + { + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], + }, + ], + }, + }, + }) + .expect(200); + kibanaServer.savedObjects.create({ + id: syntheticsApiKeyID, + type: syntheticsApiKeyObjectType, + overwrite: true, + attributes: { + id: apiKeyResult.body.id, + name: 'synthetics-api-key (required for monitor management)', + apiKey: apiKeyResult.body.api_key, + }, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).not.eql(correctPrivileges); + + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables api key when invalidated`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // delete api key + await esSupertest + .delete(`/_security/api_key`) + .send({ + ids: [validApiKeys[0].id], + }) + .expect(200); + + const validApiKeysAferDeletion = await getApiKeys(); + expect(validApiKeysAferDeletion.length).eql(0); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { await security.user.delete(username); await security.role.delete(roleName); } }); - it('with an uptime user', async () => { + it('returns response for an uptime all user without admin privileges', async () => { const username = 'uptime'; - const roleName = `uptime_user`; + const roleName = 'uptime_user'; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -198,16 +403,12 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, @@ -216,13 +417,19 @@ export default function ({ getService }: FtrProviderContext) { isValidApiKey: false, }); } finally { - await security.user.delete(username); await security.role.delete(roleName); + await security.user.delete(username); } }); }); - describe('[DELETE] - /internal/uptime/service/enablement', () => { + describe('[DELETE] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); it('with an admin', async () => { const username = 'admin'; const roleName = `synthetics_admin`; @@ -238,7 +445,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -250,7 +457,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -261,14 +468,14 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(delResponse.body).eql({}); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -303,7 +510,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertestWithAuth - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .set('kbn-xsrf', 'true') .expect(200); await supertest @@ -312,7 +519,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -351,7 +558,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -364,21 +571,21 @@ export default function ({ getService }: FtrProviderContext) { // can enable synthetics in default space when enabled in a non default space const apiResponseGet = await supertest - .get(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponseGet.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); await supertest - .post(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -388,14 +595,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -403,7 +610,7 @@ export default function ({ getService }: FtrProviderContext) { // can disable synthetics in non default space when enabled in default space await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -413,14 +620,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse2 = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse2.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -428,6 +635,7 @@ export default function ({ getService }: FtrProviderContext) { } finally { await security.user.delete(username); await security.role.delete(roleName); + await kibanaServer.spaces.delete(SPACE_ID); } }); }); From b9551e2cf0281509781b4bc0b71a845d03e8600c Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Mon, 24 Apr 2023 23:05:00 +0100 Subject: [PATCH 26/36] Sec Telemetry: Add Kubernetes and misc fields to filterlist (#152129) ## Summary Adds Kubernetes and other fields to the telemetry allowlist. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Terrance DeJesus <99630311+terrancedejesus@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Isai <59296946+imays11@users.noreply.github.com> --- .../lib/telemetry/filterlists/index.test.ts | 21 +++++ .../filterlists/prebuilt_rules_alerts.ts | 87 +++++++++++++++++ .../server/lib/telemetry/helpers.test.ts | 94 +++++++++++++++++++ .../server/lib/telemetry/helpers.ts | 47 +++++++++- .../telemetry/tasks/prebuilt_rule_alerts.ts | 9 +- .../server/lib/telemetry/types.ts | 13 +++ 6 files changed, 268 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts index 9e5c383e825d1..631b67cd49601 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts @@ -26,6 +26,9 @@ describe('Security Telemetry filters', () => { 'event.provider': true, 'event.type': true, 'powershell.file.script_block_text': true, + 'kubernetes.pod.uid': true, + 'kubernetes.pod.name': true, + 'kubernetes.pod.ip': true, package_version: true, }; @@ -177,5 +180,23 @@ describe('Security Telemetry filters', () => { package_version: '3.4.1', }); }); + + it('copies over kubernetes fields', () => { + const event = { + not_event: 'much data, much wow', + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts index 225206cca4b0d..42235cae66574 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts @@ -215,6 +215,9 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { target_resources: true, }, }, + properties: { + category: true, + }, signinlogs: { properties: { app_display_name: true, @@ -253,6 +256,85 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { setting: { name: true, }, + application: { + name: true, + }, + old_value: true, + role: { + name: true, + }, + }, + event: { + type: true, + }, + }, + // kubernetes + kubernetes: { + audit: { + annotations: true, + verb: true, + user: { + groups: true, + }, + impersonatedUser: { + groups: true, + }, + objectRef: { + name: true, + namespace: true, + resource: true, + subresource: true, + }, + requestObject: { + spec: { + containers: { + image: true, + securityContext: { + allowPrivilegeEscalation: true, + capabilities: { + add: true, + }, + privileged: true, + procMount: true, + runAsGroup: true, + runAsUser: true, + }, + }, + hostIPC: true, + hostNetwork: true, + hostPID: true, + securityContext: { + runAsGroup: true, + runAsUser: true, + }, + serviceAccountName: true, + type: true, + volumes: { + hostPath: { + path: true, + }, + }, + }, + }, + requestURI: true, + responseObject: { + roleRef: { + kind: true, + resourceName: true, + }, + rules: true, + spec: { + containers: { + securityContext: { + allowPrivilegeEscalation: true, + }, + }, + }, + }, + responseStatus: { + code: true, + }, + userAgent: true, }, }, // office 360 @@ -275,6 +357,11 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { Enabled: true, ForwardAsAttachmentTo: true, ForwardTo: true, + ModifiedProperties: { + Role_DisplayName: { + NewValue: true, + }, + }, RedirectTo: true, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts index 4e3db8c657e04..e01eb21cbe68c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts @@ -25,6 +25,7 @@ import { tlog, setIsElasticCloudDeployment, createTaskMetric, + processK8sUsernames, } from './helpers'; import type { ESClusterInfo, ESLicense, ExceptionListItem } from './types'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; @@ -963,6 +964,7 @@ describe.skip('test create task metrics', () => { passed: true, }); }); + test('can succeed when error given', async () => { const stubTaskName = 'test'; const stubPassed = false; @@ -982,3 +984,95 @@ describe.skip('test create task metrics', () => { }); }); }); + +describe('Pii is removed from a kubernetes prebuilt rule alert', () => { + test('a document without the sensitive values is ignored', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + audit: {}, + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + }, + powershell: { + command_line: 'test', + module: 'test', + module_loaded: 'test', + module_version: 'test', + process_name: 'test', + }, + }; + + const ignoredDocument = processK8sUsernames(clusterUuid, testDocument); + expect(ignoredDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are not sanitized from a document', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + impersonated_user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are sanitized from a document when not system users', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'user1', + groups: ['group1', 'group2', 'group3'], + }, + impersonated_user: { + username: 'impersonatedUser1', + groups: ['group4', 'group5', 'group6'], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index f03621899c800..f5d6bc41ee349 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -8,8 +8,9 @@ import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; -import { merge } from 'lodash'; +import { merge, set } from 'lodash'; import type { Logger } from '@kbn/core/server'; +import { sha256 } from 'js-sha256'; import { copyAllowlistedFields, filterList } from './filterlists'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; import type { @@ -300,3 +301,47 @@ export const createTaskMetric = ( error_message: errorMessage, }; }; + +function obfuscateString(clusterId: string, toHash: string): string { + const valueToObfuscate = toHash + clusterId; + return sha256.create().update(valueToObfuscate).hex(); +} + +function isAllowlistK8sUsername(username: string) { + return ( + username === 'edit' || + username === 'view' || + username === 'admin' || + username === 'elastic-agent' || + username === 'cluster-admin' || + username.startsWith('system') + ); +} + +export const processK8sUsernames = (clusterId: string, event: TelemetryEvent): TelemetryEvent => { + // if there is no kubernetes key, return the event as is + if (event.kubernetes === undefined && event.kubernetes === null) { + return event; + } + + const username = event?.kubernetes?.audit?.user?.username; + const impersonatedUser = event?.kubernetes?.audit?.impersonated_user?.username; + + if (username !== undefined && username !== null && !isAllowlistK8sUsername(username)) { + set(event, 'kubernetes.audit.user.username', obfuscateString(clusterId, username)); + } + + if ( + impersonatedUser !== undefined && + impersonatedUser !== null && + !isAllowlistK8sUsername(impersonatedUser) + ) { + set( + event, + 'kubernetes.audit.impersonated_user.username', + obfuscateString(clusterId, impersonatedUser) + ); + } + + return event; +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index a7fab953dad38..0fdc6cf32a69c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -11,7 +11,7 @@ import type { ITelemetryReceiver } from '../receiver'; import type { ESClusterInfo, ESLicense, TelemetryEvent } from '../types'; import type { TaskExecutionPeriod } from '../task'; import { TELEMETRY_CHANNEL_DETECTION_ALERTS, TASK_METRICS_CHANNEL } from '../constants'; -import { batchTelemetryRecords, tlog, createTaskMetric } from '../helpers'; +import { batchTelemetryRecords, createTaskMetric, processK8sUsernames, tlog } from '../helpers'; import { copyAllowlistedFields, filterList } from '../filterlists'; export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: number) { @@ -70,7 +70,12 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) ); - const enrichedAlerts = processedAlerts.map( + const sanitizedAlerts = processedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => + processK8sUsernames(clusterInfo?.cluster_uuid, event) + ); + + const enrichedAlerts = sanitizedAlerts.map( (event: TelemetryEvent): TelemetryEvent => ({ ...event, licence_id: licenseInfo?.uid, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index ba61f6b85aaab..df3b571714b29 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -64,6 +64,19 @@ export interface TelemetryEvent { id?: string; kind?: string; }; + kubernetes?: { + audit?: { + user?: { + username?: string; + groups?: string[]; + }; + impersonated_user?: { + username?: string; + groups?: string[]; + }; + pod?: SearchTypes; + }; + }; } // EP Policy Response From d5f12ac22fd505843990593ee5954e89202224e0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 24 Apr 2023 16:20:20 -0600 Subject: [PATCH 27/36] [ML] Data Frame Analytics custom URLs: adds ability to set custom time range in urls (#155337) ## Summary Related meta issue: https://github.com/elastic/kibana/issues/150375 This PR adds a custom time range picker to the custom urls UI for Data Frame Analytics jobs. When not selected, the timerange will default to the global timerange - this is the same behavior as before. image image image ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../custom_time_range_picker.tsx | 156 ++++++++++++++++++ .../custom_urls/custom_url_editor/editor.tsx | 107 +++++++----- .../custom_urls/custom_url_editor/utils.ts | 28 +++- .../components/custom_urls/custom_urls.tsx | 4 + 4 files changed, 249 insertions(+), 46 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx new file mode 100644 index 0000000000000..620aabd1c842b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx @@ -0,0 +1,156 @@ +/* + * 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 React, { FC, useMemo, useState } from 'react'; +import moment, { type Moment } from 'moment'; +import { + EuiDatePicker, + EuiDatePickerRange, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiIconTip, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../contexts/kibana'; + +interface CustomUrlTimeRangePickerProps { + onCustomTimeRangeChange: (customTimeRange?: { start: Moment; end: Moment }) => void; + customTimeRange?: { start: Moment; end: Moment }; +} + +/* + * React component for the form for adding a custom time range. + */ +export const CustomTimeRangePicker: FC = ({ + onCustomTimeRangeChange, + customTimeRange, +}) => { + const [showCustomTimeRangeSelector, setShowCustomTimeRangeSelector] = useState(false); + const { + services: { + data: { + query: { + timefilter: { timefilter }, + }, + }, + }, + } = useMlKibana(); + + const onCustomTimeRangeSwitchChange = (checked: boolean) => { + if (checked === false) { + // Clear the custom time range so it isn't persisted + onCustomTimeRangeChange(undefined); + } + setShowCustomTimeRangeSelector(checked); + }; + + // If the custom time range is not set, default to the timefilter settings + const currentTimeRange = useMemo( + () => + customTimeRange ?? { + start: moment(timefilter.getAbsoluteTime().from), + end: moment(timefilter.getAbsoluteTime().to), + }, + [customTimeRange, timefilter] + ); + + const handleStartChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, start: date }); + }; + const handleEndChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, end: date }); + }; + + const { start, end } = currentTimeRange; + + return ( + <> + + + + + + + + } + > + + } + checked={showCustomTimeRangeSelector} + onChange={(e) => onCustomTimeRangeSwitchChange(e.target.checked)} + compressed + /> + + + + {showCustomTimeRangeSelector ? ( + <> + + + } + > + end} + startDateControl={ + + } + endDateControl={ + + } + /> + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx index 315c60fab6a6f..523f59c32f224 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { ChangeEvent, useMemo, useState, useRef, useEffect, FC } from 'react'; +import React, { ChangeEvent, useState, useRef, useEffect, FC } from 'react'; +import { type Moment } from 'moment'; import { EuiComboBox, @@ -29,10 +30,11 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { CustomUrlSettings, isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; import { type DataFrameAnalyticsConfig } from '../../../../../common/types/data_frame_analytics'; -import { Job, isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { type Job } from '../../../../../common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE, TimeRangeType, URL_TYPE } from './constants'; import { UrlConfig } from '../../../../../common/types/custom_urls'; +import { CustomTimeRangePicker } from './custom_time_range_picker'; import { useMlKibana } from '../../../contexts/kibana'; import { getDropDownOptions } from './get_dropdown_options'; @@ -66,6 +68,7 @@ interface CustomUrlEditorProps { dashboards: Array<{ id: string; title: string }>; dataViewListItems: DataViewListItem[]; showTimeRangeSelector?: boolean; + showCustomTimeRangeSelector: boolean; job: Job | DataFrameAnalyticsConfig; } @@ -78,10 +81,12 @@ export const CustomUrlEditor: FC = ({ savedCustomUrls, dashboards, dataViewListItems, + showTimeRangeSelector, + showCustomTimeRangeSelector, job, }) => { const [queryEntityFieldNames, setQueryEntityFieldNames] = useState([]); - const isAnomalyJob = useMemo(() => isAnomalyDetectionJob(job), [job]); + const [hasTimefield, setHasTimefield] = useState(false); const { services: { @@ -101,6 +106,9 @@ export const CustomUrlEditor: FC = ({ } catch (e) { dataViewToUse = undefined; } + if (dataViewToUse && dataViewToUse.timeFieldName) { + setHasTimefield(true); + } const dropDownOptions = await getDropDownOptions(isFirst.current, job, dataViewToUse); setQueryEntityFieldNames(dropDownOptions); @@ -132,6 +140,13 @@ export const CustomUrlEditor: FC = ({ }); }; + const onCustomTimeRangeChange = (timeRange?: { start: Moment; end: Moment }) => { + setEditCustomUrl({ + ...customUrl, + customTimeRange: timeRange, + }); + }; + const onDashboardChange = (e: ChangeEvent) => { const kibanaSettings = customUrl.kibanaSettings; setEditCustomUrl({ @@ -345,58 +360,66 @@ export const CustomUrlEditor: FC = ({ /> )} + {type === URL_TYPE.KIBANA_DASHBOARD || + (type === URL_TYPE.KIBANA_DISCOVER && showCustomTimeRangeSelector && hasTimefield) ? ( + + ) : null} - {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && isAnomalyJob && ( - <> - - - - - } - className="url-time-range" - display="rowCompressed" - > - - - - {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( - + {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && + showTimeRangeSelector && ( + <> + + + } className="url-time-range" - error={invalidIntervalError} - isInvalid={isInvalidTimeRange} display="rowCompressed" > - - )} - - - )} + {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( + + + } + className="url-time-range" + error={invalidIntervalError} + isInvalid={isInvalidTimeRange} + display="rowCompressed" + > + + + + )} + + + )} {type === URL_TYPE.OTHER && ( { // Get the complete list of attributes for the selected dashboard (query, filters). const { dashboardId, queryFieldNames } = settings.kibanaSettings ?? {}; @@ -253,11 +269,13 @@ async function buildDashboardUrlFromSettings(settings: CustomUrlSettings): Promi const dashboard = getDashboard(); + const { from, to } = getUrlRangeFromSettings(settings); + const location = await dashboard?.locator?.getLocation({ dashboardId, timeRange: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, filters, @@ -299,10 +317,12 @@ function buildDiscoverUrlFromSettings(settings: CustomUrlSettings) { // Add time settings to the global state URL parameter with $earliest$ and // $latest$ tokens which get substituted for times around the time of the // anomaly on which the URL will be run against. + const { from, to } = getUrlRangeFromSettings(settings); + const _g = rison.encode({ time: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, }); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 9d3db04fa40de..4f9ad5245cf91 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -42,6 +42,8 @@ import { import { openCustomUrlWindow } from '../../util/custom_url_utils'; import { UrlConfig } from '../../../../common/types/custom_urls'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; +import { isAnomalyDetectionJob } from '../../../../common/types/anomaly_detection_jobs'; +import { isDataFrameAnalyticsConfigs } from '../../../../common/types/data_frame_analytics'; const MAX_NUMBER_DASHBOARDS = 1000; @@ -206,6 +208,8 @@ class CustomUrlsUI extends Component { const editMode = this.props.editMode ?? 'inline'; const editor = ( Date: Tue, 25 Apr 2023 01:12:32 +0200 Subject: [PATCH 28/36] [serverless-docker] Allow using SERVERLESS env var (#155670) --- .../docker_generator/resources/base/bin/kibana-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index b08d26b8c657d..3123aa7f391df 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -414,6 +414,7 @@ kibana_vars=( xpack.task_manager.event_loop_delay.monitor xpack.task_manager.event_loop_delay.warn_threshold xpack.uptime.index + serverless ) longopts='' From 675ed0eee2eb582321bfb9b379783c973a490bd2 Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Tue, 25 Apr 2023 01:22:56 +0200 Subject: [PATCH 29/36] [Security Solution] Add active maintenance window callout to the Rules Management page (#155386) **Addresses:** https://github.com/elastic/kibana/issues/155099 **Documentation issue:** https://github.com/elastic/security-docs/issues/3181 ## Summary Adds a Maintenance Window callout to the Rules Management page. This callout is only displayed when a maintenance window is running. Screenshot 2023-04-21 at 13 24 11 ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) issue created: https://github.com/elastic/security-docs/issues/3181 - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Georgii Gorbachev --- x-pack/plugins/alerting/common/index.ts | 6 + .../alerting/common/maintenance_window.ts | 5 + .../active_maintenance_windows.ts | 7 +- .../archive_maintenance_window.ts | 7 +- .../create_maintenance_window.ts | 14 +- .../delete_maintenance_window.ts | 7 +- .../find_maintenance_windows.ts | 7 +- .../finish_maintenance_window.ts | 7 +- .../get_maintenance_window.ts | 7 +- .../update_maintenance_window.ts | 7 +- .../detection_rules/maintenance_window.cy.ts | 58 ++++++++ .../security_solution/cypress/tsconfig.json | 3 +- .../maintenance_window_callout/api.ts | 18 +++ .../maintenance_window_callout.test.tsx | 136 ++++++++++++++++++ .../maintenance_window_callout.tsx | 27 ++++ .../translations.ts | 36 +++++ .../use_fetch_active_maintenance_windows.ts | 27 ++++ .../pages/rule_management/index.tsx | 3 + 18 files changed, 359 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/e2e/detection_rules/maintenance_window.cy.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/api.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/use_fetch_active_maintenance_windows.ts diff --git a/x-pack/plugins/alerting/common/index.ts b/x-pack/plugins/alerting/common/index.ts index 8b4c04d15cfc4..1fa0806effdef 100644 --- a/x-pack/plugins/alerting/common/index.ts +++ b/x-pack/plugins/alerting/common/index.ts @@ -58,6 +58,12 @@ export const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; export const BASE_ALERTING_API_PATH = '/api/alerting'; export const INTERNAL_BASE_ALERTING_API_PATH = '/internal/alerting'; export const INTERNAL_ALERTING_API_FIND_RULES_PATH = `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_find`; + +export const INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH = + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window` as const; +export const INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH = + `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/_active` as const; + export const ALERTS_FEATURE_ID = 'alerts'; export const MONITORING_HISTORY_LIMIT = 200; export const ENABLE_MAINTENANCE_WINDOWS = false; diff --git a/x-pack/plugins/alerting/common/maintenance_window.ts b/x-pack/plugins/alerting/common/maintenance_window.ts index 0392d0cdb3667..e41140f8fc918 100644 --- a/x-pack/plugins/alerting/common/maintenance_window.ts +++ b/x-pack/plugins/alerting/common/maintenance_window.ts @@ -46,6 +46,11 @@ export type MaintenanceWindow = MaintenanceWindowSOAttributes & { id: string; }; +export type MaintenanceWindowCreateBody = Omit< + MaintenanceWindowSOProperties, + 'events' | 'expirationDate' | 'enabled' | 'archived' +>; + export interface MaintenanceWindowClientContext { getModificationMetadata: () => Promise; savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts index 706630edfbd4a..8bd3f7d3e0b49 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/active_maintenance_windows.ts @@ -8,7 +8,10 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; export const activeMaintenanceWindowsRoute = ( @@ -17,7 +20,7 @@ export const activeMaintenanceWindowsRoute = ( ) => { router.get( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`, + path: INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, validate: {}, options: { tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`], diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/archive_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/archive_maintenance_window.ts index 123f374f79b05..e46bc07463e2f 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/archive_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/archive_maintenance_window.ts @@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const paramSchema = schema.object({ @@ -26,7 +29,7 @@ export const archiveMaintenanceWindowRoute = ( ) => { router.post( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_archive`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}/_archive`, validate: { params: paramSchema, body: bodySchema, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/create_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/create_maintenance_window.ts index a74147d15890c..d26f8494e1061 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/create_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/create_maintenance_window.ts @@ -14,8 +14,11 @@ import { RewriteRequestCase, rewriteMaintenanceWindowRes, } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; -import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; +import { MaintenanceWindowCreateBody, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const bodySchema = schema.object({ title: schema.string(), @@ -23,11 +26,6 @@ const bodySchema = schema.object({ r_rule: rRuleSchema, }); -type MaintenanceWindowCreateBody = Omit< - MaintenanceWindowSOProperties, - 'events' | 'expirationDate' | 'enabled' | 'archived' ->; - export const rewriteQueryReq: RewriteRequestCase = ({ r_rule: rRule, ...rest @@ -42,7 +40,7 @@ export const createMaintenanceWindowRoute = ( ) => { router.post( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window`, + path: INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, validate: { body: bodySchema, }, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/delete_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/delete_maintenance_window.ts index 2415dbe74b53d..c9ea00ef170de 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/delete_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/delete_maintenance_window.ts @@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const paramSchema = schema.object({ @@ -22,7 +25,7 @@ export const deleteMaintenanceWindowRoute = ( ) => { router.delete( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/find_maintenance_windows.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/find_maintenance_windows.ts index b581a011630a9..e9262b3e51079 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/find_maintenance_windows.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/find_maintenance_windows.ts @@ -8,7 +8,10 @@ import { IRouter } from '@kbn/core/server'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; export const findMaintenanceWindowsRoute = ( @@ -17,7 +20,7 @@ export const findMaintenanceWindowsRoute = ( ) => { router.get( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_find`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/_find`, validate: {}, options: { tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`], diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/finish_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/finish_maintenance_window.ts index 2cd5ff9ba0994..0cb663043d57a 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/finish_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/finish_maintenance_window.ts @@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const paramSchema = schema.object({ @@ -22,7 +25,7 @@ export const finishMaintenanceWindowRoute = ( ) => { router.post( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_finish`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}/_finish`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/get_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/get_maintenance_window.ts index dc01beeef148a..b92281373817d 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/get_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/get_maintenance_window.ts @@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ILicenseState } from '../../lib'; import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const paramSchema = schema.object({ @@ -22,7 +25,7 @@ export const getMaintenanceWindowRoute = ( ) => { router.get( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`, validate: { params: paramSchema, }, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/update_maintenance_window.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/update_maintenance_window.ts index 7778b4d621359..5e63624587152 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/update_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/update_maintenance_window.ts @@ -14,7 +14,10 @@ import { RewriteRequestCase, rewritePartialMaintenanceBodyRes, } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, +} from '../../types'; import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common'; const paramSchema = schema.object({ @@ -49,7 +52,7 @@ export const updateMaintenanceWindowRoute = ( ) => { router.post( { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`, + path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`, validate: { body: bodySchema, params: paramSchema, diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/maintenance_window.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/maintenance_window.cy.ts new file mode 100644 index 0000000000000..af534d325ebca --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/maintenance_window.cy.ts @@ -0,0 +1,58 @@ +/* + * 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 { INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH } from '@kbn/alerting-plugin/common'; +import type { MaintenanceWindowCreateBody } from '@kbn/alerting-plugin/common'; +import type { AsApiContract } from '@kbn/alerting-plugin/server/routes/lib'; +import { cleanKibana } from '../../tasks/common'; +import { login, visit } from '../../tasks/login'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; + +describe('Maintenance window callout on Rule Management page', () => { + let maintenanceWindowId = ''; + + before(() => { + cleanKibana(); + login(); + + const body: AsApiContract = { + title: 'My maintenance window', + duration: 60000, // 1 minute + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'Europe/Amsterdam', + freq: 0, + count: 1, + }, + }; + + // Create a test maintenance window + cy.request({ + method: 'POST', + url: INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH, + headers: { 'kbn-xsrf': 'cypress-creds' }, + body, + }).then((response) => { + maintenanceWindowId = response.body.id; + }); + }); + + after(() => { + // Delete a test maintenance window + cy.request({ + method: 'DELETE', + url: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/${maintenanceWindowId}`, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); + }); + + it('Displays the callout when there are running maintenance windows', () => { + visit(DETECTIONS_RULE_MANAGEMENT_URL); + + cy.contains('A maintenance window is currently running'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index 4af63c6d1b406..4f7ce1b811f70 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -28,6 +28,7 @@ "force": true }, "@kbn/rison", - "@kbn/datemath" + "@kbn/datemath", + "@kbn/alerting-plugin" ] } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/api.ts new file mode 100644 index 0000000000000..9d3c28429df5b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/api.ts @@ -0,0 +1,18 @@ +/* + * 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 type { MaintenanceWindow } from '@kbn/alerting-plugin/common/maintenance_window'; +import { INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH } from '@kbn/alerting-plugin/common'; +import { KibanaServices } from '../../../../common/lib/kibana'; + +export const fetchActiveMaintenanceWindows = async ( + signal?: AbortSignal +): Promise => + KibanaServices.get().http.fetch(INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, { + method: 'GET', + signal, + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.test.tsx new file mode 100644 index 0000000000000..20a8e0d1f2f94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.test.tsx @@ -0,0 +1,136 @@ +/* + * 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 React from 'react'; +import { render, waitFor, cleanup } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common'; +import type { MaintenanceWindow } from '@kbn/alerting-plugin/common'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; +import { MaintenanceWindowCallout } from './maintenance_window_callout'; +import { TestProviders } from '../../../../common/mock'; +import { fetchActiveMaintenanceWindows } from './api'; + +jest.mock('../../../../common/hooks/use_app_toasts'); + +jest.mock('./api', () => ({ + fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])), +})); + +const RUNNING_MAINTENANCE_WINDOW_1: Partial = { + title: 'Maintenance window 1', + id: '63057284-ac31-42ba-fe22-adfe9732e5ae', + status: MaintenanceWindowStatus.Running, + events: [{ gte: '2023-04-20T16:27:30.753Z', lte: '2023-04-20T16:57:30.753Z' }], +}; + +const RUNNING_MAINTENANCE_WINDOW_2: Partial = { + title: 'Maintenance window 2', + id: '45894340-df98-11ed-ac81-bfcb4982b4fd', + status: MaintenanceWindowStatus.Running, + events: [{ gte: '2023-04-20T16:47:42.871Z', lte: '2023-04-20T17:11:32.192Z' }], +}; + +const UPCOMING_MAINTENANCE_WINDOW: Partial = { + title: 'Upcoming maintenance window', + id: '5eafe070-e030-11ed-ac81-bfcb4982b4fd', + status: MaintenanceWindowStatus.Upcoming, + events: [ + { gte: '2023-04-21T10:36:14.028Z', lte: '2023-04-21T10:37:00.000Z' }, + { gte: '2023-04-28T10:36:14.028Z', lte: '2023-04-28T10:37:00.000Z' }, + ], +}; + +describe('MaintenanceWindowCallout', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + jest.resetAllMocks(); + + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + + afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + }); + + it('should be visible if currently there is at least one "running" maintenance window', async () => { + (fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]); + + const { findByText } = render(, { wrapper: TestProviders }); + + expect(await findByText('A maintenance window is currently running')).toBeInTheDocument(); + }); + + it('should be visible if currently there are multiple "running" maintenance windows', async () => { + (fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([ + RUNNING_MAINTENANCE_WINDOW_1, + RUNNING_MAINTENANCE_WINDOW_2, + ]); + + const { findAllByText } = render(, { wrapper: TestProviders }); + + expect(await findAllByText('A maintenance window is currently running')).toHaveLength(1); + }); + + it('should NOT be visible if currently there are no active (running or upcoming) maintenance windows', async () => { + (fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([]); + + const { container } = render(, { wrapper: TestProviders }); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should NOT be visible if currently there are no "running" maintenance windows', async () => { + (fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]); + + const { container } = render(, { wrapper: TestProviders }); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should see an error toast if there was an error while fetching maintenance windows', async () => { + const createReactQueryWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Turn retries off, otherwise we won't be able to test errors + retry: false, + }, + }, + logger: { + // Turn network error logging off, so we don't log the failed request to the console + error: () => {}, + // eslint-disable-next-line no-console + log: console.log, + // eslint-disable-next-line no-console + warn: console.warn, + }, + }); + const wrapper: React.FC = ({ children }) => ( + {children} + ); + return wrapper; + }; + + const mockError = new Error('Network error'); + (fetchActiveMaintenanceWindows as jest.Mock).mockRejectedValue(mockError); + + render(, { wrapper: createReactQueryWrapper() }); + + await waitFor(() => { + expect(appToastsMock.addError).toHaveBeenCalledTimes(1); + expect(appToastsMock.addError).toHaveBeenCalledWith(mockError, { + title: 'Failed to check if any maintenance window is currently running', + toastMessage: "Notification actions won't run while a maintenance window is running.", + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.tsx new file mode 100644 index 0000000000000..878347dc37c98 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/maintenance_window_callout.tsx @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common'; +import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows'; +import * as i18n from './translations'; + +export function MaintenanceWindowCallout(): JSX.Element | null { + const { data } = useFetchActiveMaintenanceWindows(); + const activeMaintenanceWindows = data || []; + + if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) { + return ( + + {i18n.MAINTENANCE_WINDOW_RUNNING_DESCRIPTION} + + ); + } + + return null; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/translations.ts new file mode 100644 index 0000000000000..21071aee618a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/translations.ts @@ -0,0 +1,36 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const MAINTENANCE_WINDOW_RUNNING = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActive', + { + defaultMessage: 'A maintenance window is currently running', + } +); + +export const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActiveDescription', + { + defaultMessage: "Notification actions won't run while a maintenance window is running.", + } +); + +export const FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchError', + { + defaultMessage: 'Failed to check if any maintenance window is currently running', + } +); + +export const FETCH_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchErrorDescription', + { + defaultMessage: "Notification actions won't run while a maintenance window is running.", + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/use_fetch_active_maintenance_windows.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/use_fetch_active_maintenance_windows.ts new file mode 100644 index 0000000000000..3603cafbda935 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/maintenance_window_callout/use_fetch_active_maintenance_windows.ts @@ -0,0 +1,27 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH } from '@kbn/alerting-plugin/common'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import * as i18n from './translations'; +import { fetchActiveMaintenanceWindows } from './api'; + +export const useFetchActiveMaintenanceWindows = () => { + const { addError } = useAppToasts(); + + return useQuery( + ['GET', INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH], + ({ signal }) => fetchActiveMaintenanceWindows(signal), + { + refetchInterval: 60000, + onError: (error) => { + addError(error, { title: i18n.FETCH_ERROR, toastMessage: i18n.FETCH_ERROR_DESCRIPTION }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 95e0857874423..03231f2030eb2 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -42,6 +42,8 @@ import { RulesTableContextProvider } from '../../components/rules_table/rules_ta import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; import { useInvalidateFetchRuleManagementFiltersQuery } from '../../../rule_management/api/hooks/use_fetch_rule_management_filters_query'; +import { MaintenanceWindowCallout } from '../../components/maintenance_window_callout/maintenance_window_callout'; + const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); const [isValueListFlyoutVisible, showValueListFlyout, hideValueListFlyout] = useBoolState(); @@ -158,6 +160,7 @@ const RulesPageComponent: React.FC = () => { prePackagedTimelineStatus === 'timelineNeedUpdate') && ( )} + From 402085882773e450582e238bb04ede62807a5404 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 24 Apr 2023 18:54:41 -0700 Subject: [PATCH 30/36] [Shared UX] Clean up toolbar styles (#155667) ## Summary Closes #155668. This PR fixes a few minor visual buttons with the `Toolbar` component styles. #### Before Screenshot 2023-04-24 at 2 41 16 PM #### After Screenshot 2023-04-24 at 2 39 09 PM Changes: - apply same 8px gap between all toolbar buttons - remove shadow from icon button group buttons causing the double border between buttons - fix border radius on button group to match other toolbar buttons ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../__snapshots__/icon_button_group.test.tsx.snap | 4 ++-- .../buttons/icon_button_group/icon_button_group.styles.ts | 8 ++++++++ .../src/buttons/icon_button_group/icon_button_group.tsx | 1 + .../src/toolbar/__snapshots__/toolbar.test.tsx.snap | 2 +- packages/shared-ux/button_toolbar/src/toolbar/toolbar.tsx | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/shared-ux/button_toolbar/src/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap b/packages/shared-ux/button_toolbar/src/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap index 3780fa1bcddd6..c472f58ec3e2f 100644 --- a/packages/shared-ux/button_toolbar/src/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap +++ b/packages/shared-ux/button_toolbar/src/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap @@ -2,7 +2,7 @@ exports[` is rendered 1`] = `
is rendered 1`] = `
diff --git a/packages/shared-ux/button_toolbar/src/toolbar/toolbar.tsx b/packages/shared-ux/button_toolbar/src/toolbar/toolbar.tsx index eef0ce05eed6e..f5b5c2bce0cd0 100644 --- a/packages/shared-ux/button_toolbar/src/toolbar/toolbar.tsx +++ b/packages/shared-ux/button_toolbar/src/toolbar/toolbar.tsx @@ -59,7 +59,7 @@ export const Toolbar = ({ children }: Props) => { {primaryButton} - + {iconButtonGroup ? {iconButtonGroup} : null} {extra} From 273eec0f64dceab0ac2589bb31264d7533f6e942 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 24 Apr 2023 20:58:22 -0500 Subject: [PATCH 31/36] [content management / maps] Create abstract types for saved object usage with content management api (#154985) ## Summary Abstract types for using Saved Objects with the content management api. This should significantly reduce the amount of code to use additional saved object types. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + .../kbn-content-management-utils/README.md | 58 ++++ .../kbn-content-management-utils/index.ts | 9 + .../jest.config.js | 13 + .../kbn-content-management-utils/kibana.jsonc | 5 + .../kbn-content-management-utils/package.json | 6 + .../tsconfig.json | 22 ++ .../kbn-content-management-utils/types.ts | 283 ++++++++++++++++++ tsconfig.base.json | 2 + .../maps/common/content_management/index.ts | 4 +- .../common/content_management/v1/index.ts | 4 +- .../common/content_management/v1/types.ts | 104 ++----- .../public/content_management/maps_client.ts | 11 +- .../server/content_management/maps_storage.ts | 16 +- x-pack/plugins/maps/tsconfig.json | 1 + yarn.lock | 4 + 17 files changed, 452 insertions(+), 92 deletions(-) create mode 100644 packages/kbn-content-management-utils/README.md create mode 100644 packages/kbn-content-management-utils/index.ts create mode 100644 packages/kbn-content-management-utils/jest.config.js create mode 100644 packages/kbn-content-management-utils/kibana.jsonc create mode 100644 packages/kbn-content-management-utils/package.json create mode 100644 packages/kbn-content-management-utils/tsconfig.json create mode 100644 packages/kbn-content-management-utils/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 63cf2327516fa..2da78ed653a0d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,6 +88,7 @@ packages/content-management/content_editor @elastic/appex-sharedux examples/content_management_examples @elastic/appex-sharedux src/plugins/content_management @elastic/appex-sharedux packages/content-management/table_list @elastic/appex-sharedux +packages/kbn-content-management-utils @elastic/kibana-data-discovery examples/controls_example @elastic/kibana-presentation src/plugins/controls @elastic/kibana-presentation src/core @elastic/kibana-core diff --git a/package.json b/package.json index 56fdffa498b05..06bec180ca93b 100644 --- a/package.json +++ b/package.json @@ -187,6 +187,7 @@ "@kbn/content-management-examples-plugin": "link:examples/content_management_examples", "@kbn/content-management-plugin": "link:src/plugins/content_management", "@kbn/content-management-table-list": "link:packages/content-management/table_list", + "@kbn/content-management-utils": "link:packages/kbn-content-management-utils", "@kbn/controls-example-plugin": "link:examples/controls_example", "@kbn/controls-plugin": "link:src/plugins/controls", "@kbn/core": "link:src/core", diff --git a/packages/kbn-content-management-utils/README.md b/packages/kbn-content-management-utils/README.md new file mode 100644 index 0000000000000..32c28b9fc4807 --- /dev/null +++ b/packages/kbn-content-management-utils/README.md @@ -0,0 +1,58 @@ +# Content management utils + +Utilities to ease the implementation of the Content Management API with Saved Objects. + +```ts +import type { + ContentManagementCrudTypes, + CreateOptions, + SearchOptions, + UpdateOptions, +} from '@kbn/content-management-utils'; +import { MapContentType } from '../types'; + +export type MapCrudTypes = ContentManagementCrudTypes; + +/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */ +export type MapAttributes = { + title: string; + description?: string; + mapStateJSON?: string; + layerListJSON?: string; + uiStateJSON?: string; +}; + +export type MapItem = MapCrudTypes['Item']; +export type PartialMapItem = MapCrudTypes['PartialItem']; + +// ----------- GET -------------- + +export type MapGetIn = MapCrudTypes['GetIn']; +export type MapGetOut = MapCrudTypes['GetOut']; + +// ----------- CREATE -------------- + +export type MapCreateIn = MapCrudTypes['CreateIn']; +export type MapCreateOut = MapCrudTypes['CreateOut']; +export type MapCreateOptions = CreateOptions; + +// ----------- UPDATE -------------- + +export type MapUpdateIn = MapCrudTypes['UpdateIn']; +export type MapUpdateOut = MapCrudTypes['UpdateOut']; +export type MapUpdateOptions = UpdateOptions; + +// ----------- DELETE -------------- + +export type MapDeleteIn = MapCrudTypes['DeleteIn']; +export type MapDeleteOut = MapCrudTypes['DeleteOut']; + +// ----------- SEARCH -------------- + +export type MapSearchIn = MapCrudTypes['SearchIn']; +export type MapSearchOut = MapCrudTypes['SearchOut']; +export type MapSearchOptions = SearchOptions; + +``` + + diff --git a/packages/kbn-content-management-utils/index.ts b/packages/kbn-content-management-utils/index.ts new file mode 100644 index 0000000000000..12594660136d8 --- /dev/null +++ b/packages/kbn-content-management-utils/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './types'; diff --git a/packages/kbn-content-management-utils/jest.config.js b/packages/kbn-content-management-utils/jest.config.js new file mode 100644 index 0000000000000..b1e7646521e26 --- /dev/null +++ b/packages/kbn-content-management-utils/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-content-management-utils'], +}; diff --git a/packages/kbn-content-management-utils/kibana.jsonc b/packages/kbn-content-management-utils/kibana.jsonc new file mode 100644 index 0000000000000..06779896a47c4 --- /dev/null +++ b/packages/kbn-content-management-utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/content-management-utils", + "owner": "@elastic/kibana-data-discovery" +} diff --git a/packages/kbn-content-management-utils/package.json b/packages/kbn-content-management-utils/package.json new file mode 100644 index 0000000000000..8b59a10c3d429 --- /dev/null +++ b/packages/kbn-content-management-utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/content-management-utils", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-content-management-utils/tsconfig.json b/packages/kbn-content-management-utils/tsconfig.json new file mode 100644 index 0000000000000..7de04c3c13451 --- /dev/null +++ b/packages/kbn-content-management-utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/content-management-plugin", + "@kbn/core-saved-objects-api-server", + ] +} diff --git a/packages/kbn-content-management-utils/types.ts b/packages/kbn-content-management-utils/types.ts new file mode 100644 index 0000000000000..1f15c69947f63 --- /dev/null +++ b/packages/kbn-content-management-utils/types.ts @@ -0,0 +1,283 @@ +/* + * 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 type { + GetIn, + GetResult, + CreateIn, + CreateResult, + SearchIn, + SearchResult, + UpdateIn, + UpdateResult, + DeleteIn, + DeleteResult, +} from '@kbn/content-management-plugin/common'; + +import type { + SortOrder, + AggregationsAggregationContainer, + SortResults, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { + MutatingOperationRefreshSetting, + SavedObjectsPitParams, + SavedObjectsFindOptionsReference, +} from '@kbn/core-saved-objects-api-server'; + +type KueryNode = any; + +export interface Reference { + type: string; + id: string; + name: string; +} + +/** Saved Object create options - Pick and Omit to customize */ +export interface SavedObjectCreateOptions { + /** (not recommended) Specify an id for the document */ + id?: string; + /** Overwrite existing documents (defaults to false) */ + overwrite?: boolean; + /** + * An opaque version number which changes on each successful write operation. + * Can be used in conjunction with `overwrite` for implementing optimistic concurrency control. + **/ + version?: string; + /** Array of referenced saved objects. */ + references?: Reference[]; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: boolean; + /** + * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in + * {@link SavedObjectsCreateOptions}. + * + * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, + * including the "All spaces" identifier (`'*'`). + * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only + * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. + * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. + */ + initialNamespaces?: string[]; +} + +/** Saved Object search options - Pick and Omit to customize */ +export interface SavedObjectSearchOptions { + /** the page of results to return */ + page?: number; + /** the number of objects per page */ + perPage?: number; + /** which field to sort by */ + sortField?: string; + /** sort order, ascending or descending */ + sortOrder?: SortOrder; + /** + * An array of fields to include in the results + * @example + * SavedObjects.find({type: 'dashboard', fields: ['attributes.name', 'attributes.location']}) + */ + fields?: string[]; + /** Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information */ + search?: string; + /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ + searchFields?: string[]; + /** + * Use the sort values from the previous page to retrieve the next page of results. + */ + searchAfter?: SortResults; + /** + * The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not + * be modified. If used in conjunction with `searchFields`, both are concatenated together. + */ + rootSearchFields?: string[]; + /** + * Search for documents having a reference to the specified objects. + * Use `hasReferenceOperator` to specify the operator to use when searching for multiple references. + */ + hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]; + /** + * The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR` + */ + hasReferenceOperator?: 'AND' | 'OR'; + /** + * Search for documents *not* having a reference to the specified objects. + * Use `hasNoReferenceOperator` to specify the operator to use when searching for multiple references. + */ + hasNoReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[]; + /** + * The operator to use when searching by multiple references using the `hasNoReference` option. Defaults to `OR` + */ + hasNoReferenceOperator?: 'AND' | 'OR'; + /** + * The search operator to use with the provided filter. Defaults to `OR` + */ + defaultSearchOperator?: 'AND' | 'OR'; + /** filter string for the search query */ + filter?: string | KueryNode; + /** + * A record of aggregations to perform. + * The API currently only supports a limited set of metrics and bucket aggregation types. + * Additional aggregation types can be contributed to Core. + * + * @example + * Aggregating on SO attribute field + * ```ts + * const aggs = { latest_version: { max: { field: 'dashboard.attributes.version' } } }; + * return client.find({ type: 'dashboard', aggs }) + * ``` + * + * @example + * Aggregating on SO root field + * ```ts + * const aggs = { latest_update: { max: { field: 'dashboard.updated_at' } } }; + * return client.find({ type: 'dashboard', aggs }) + * ``` + * + * @alpha + */ + aggs?: Record; + /** array of namespaces to search */ + namespaces?: string[]; + /** + * Search against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}. + */ + pit?: SavedObjectsPitParams; +} + +/** Saved Object update options - Pick and Omit to customize */ +export interface SavedObjectUpdateOptions { + /** Array of referenced saved objects. */ + references?: Reference[]; + version?: string; + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; + /** If specified, will be used to perform an upsert if the object doesn't exist */ + upsert?: Attributes; + /** + * The Elasticsearch `retry_on_conflict` setting for this operation. + * Defaults to `0` when `version` is provided, `3` otherwise. + */ + retryOnConflict?: number; +} + +/** Return value for Saved Object get, T is item returned */ +export type GetResultSO = GetResult< + T, + { + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; + aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport'; + } +>; + +/** + * Saved object with metadata + */ +export interface SOWithMetadata { + id: string; + type: string; + version?: string; + createdAt?: string; + updatedAt?: string; + error?: { + error: string; + message: string; + statusCode: number; + metadata?: Record; + }; + attributes: Attributes; + references: Reference[]; + namespaces?: string[]; + originId?: string; +} + +type PartialItem = Omit< + SOWithMetadata, + 'attributes' | 'references' +> & { + attributes: Partial; + references: Reference[] | undefined; +}; + +/** + * Types used by content management storage + * @argument ContentType - content management type. assumed to be the same as saved object type + * @argument Attributes - attributes of the saved object + */ +export interface ContentManagementCrudTypes< + ContentType extends string, + Attributes extends object, + CreateOptions extends object, + UpdateOptions extends object, + SearchOptions extends object +> { + /** + * Complete saved object + */ + Item: SOWithMetadata; + /** + * Partial saved object, used as output for update + */ + PartialItem: PartialItem; + /** + * Create options + */ + CreateOptions: CreateOptions; + /** + * Update options + */ + UpdateOptions: UpdateOptions; + /** + * Search options + */ + SearchOptions: SearchOptions; + /** + * Get item params + */ + GetIn: GetIn; + /** + * Get item result + */ + GetOut: GetResultSO>; + /** + * Create item params + */ + CreateIn: CreateIn; + /** + * Create item result + */ + CreateOut: CreateResult>; + + /** + * Search item params + */ + SearchIn: SearchIn; + /** + * Search item result + */ + SearchOut: SearchResult>; + + /** + * Update item params + */ + UpdateIn: UpdateIn; + /** + * Update item result + */ + UpdateOut: UpdateResult>; + + /** + * Delete item params + */ + DeleteIn: DeleteIn; + /** + * Delete item result + */ + DeleteOut: DeleteResult; +} diff --git a/tsconfig.base.json b/tsconfig.base.json index e96e5439e9c2c..929f3e791ad1e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -170,6 +170,8 @@ "@kbn/content-management-plugin/*": ["src/plugins/content_management/*"], "@kbn/content-management-table-list": ["packages/content-management/table_list"], "@kbn/content-management-table-list/*": ["packages/content-management/table_list/*"], + "@kbn/content-management-utils": ["packages/kbn-content-management-utils"], + "@kbn/content-management-utils/*": ["packages/kbn-content-management-utils/*"], "@kbn/controls-example-plugin": ["examples/controls_example"], "@kbn/controls-example-plugin/*": ["examples/controls_example/*"], "@kbn/controls-plugin": ["src/plugins/controls"], diff --git a/x-pack/plugins/maps/common/content_management/index.ts b/x-pack/plugins/maps/common/content_management/index.ts index daafc194a37fc..f389e3fea36e7 100644 --- a/x-pack/plugins/maps/common/content_management/index.ts +++ b/x-pack/plugins/maps/common/content_management/index.ts @@ -17,10 +17,10 @@ export type { MapGetOut, MapCreateIn, MapCreateOut, - CreateOptions, + MapCreateOptions, MapUpdateIn, MapUpdateOut, - UpdateOptions, + MapUpdateOptions, MapDeleteIn, MapDeleteOut, MapSearchIn, diff --git a/x-pack/plugins/maps/common/content_management/v1/index.ts b/x-pack/plugins/maps/common/content_management/v1/index.ts index 272e0e1eb5f2e..535a15e5a23a6 100644 --- a/x-pack/plugins/maps/common/content_management/v1/index.ts +++ b/x-pack/plugins/maps/common/content_management/v1/index.ts @@ -13,10 +13,10 @@ export type { MapGetOut, MapCreateIn, MapCreateOut, - CreateOptions, + MapCreateOptions, MapUpdateIn, MapUpdateOut, - UpdateOptions, + MapUpdateOptions, MapDeleteIn, MapDeleteOut, MapSearchIn, diff --git a/x-pack/plugins/maps/common/content_management/v1/types.ts b/x-pack/plugins/maps/common/content_management/v1/types.ts index d5a7351226c23..c19713e641659 100644 --- a/x-pack/plugins/maps/common/content_management/v1/types.ts +++ b/x-pack/plugins/maps/common/content_management/v1/types.ts @@ -6,24 +6,22 @@ */ import type { - GetIn, - GetResult, - CreateIn, - CreateResult, - SearchIn, - SearchResult, - UpdateIn, - UpdateResult, - DeleteIn, - DeleteResult, -} from '@kbn/content-management-plugin/common'; + ContentManagementCrudTypes, + SavedObjectCreateOptions, + SavedObjectUpdateOptions, +} from '@kbn/content-management-utils'; import { MapContentType } from '../types'; -interface Reference { - type: string; - id: string; - name: string; -} +export type MapCrudTypes = ContentManagementCrudTypes< + MapContentType, + MapAttributes, + Pick, + Pick, + { + /** Flag to indicate to only search the text on the "title" field */ + onlyTitle?: boolean; + } +>; /* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */ export type MapAttributes = { @@ -34,77 +32,33 @@ export type MapAttributes = { uiStateJSON?: string; }; -export interface MapItem { - id: string; - type: string; - version?: string; - createdAt?: string; - updatedAt?: string; - error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; - attributes: MapAttributes; - references: Reference[]; - namespaces?: string[]; - originId?: string; -} - -export type PartialMapItem = Omit & { - attributes: Partial; - references: Reference[] | undefined; -}; +export type MapItem = MapCrudTypes['Item']; +export type PartialMapItem = MapCrudTypes['PartialItem']; // ----------- GET -------------- -export type MapGetIn = GetIn; - -export type MapGetOut = GetResult< - MapItem, - { - outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; - aliasTargetId?: string; - aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport'; - } ->; +export type MapGetIn = MapCrudTypes['GetIn']; +export type MapGetOut = MapCrudTypes['GetOut']; // ----------- CREATE -------------- -export interface CreateOptions { - /** Array of referenced saved objects. */ - references?: Reference[]; -} - -export type MapCreateIn = CreateIn; - -export type MapCreateOut = CreateResult; +export type MapCreateIn = MapCrudTypes['CreateIn']; +export type MapCreateOut = MapCrudTypes['CreateOut']; +export type MapCreateOptions = MapCrudTypes['CreateOptions']; // ----------- UPDATE -------------- -export interface UpdateOptions { - /** Array of referenced saved objects. */ - references?: Reference[]; -} - -export type MapUpdateIn = UpdateIn; - -export type MapUpdateOut = UpdateResult; +export type MapUpdateIn = MapCrudTypes['UpdateIn']; +export type MapUpdateOut = MapCrudTypes['UpdateOut']; +export type MapUpdateOptions = MapCrudTypes['UpdateOptions']; // ----------- DELETE -------------- -export type MapDeleteIn = DeleteIn; - -export type MapDeleteOut = DeleteResult; +export type MapDeleteIn = MapCrudTypes['DeleteIn']; +export type MapDeleteOut = MapCrudTypes['DeleteOut']; // ----------- SEARCH -------------- -export interface MapSearchOptions { - /** Flag to indicate to only search the text on the "title" field */ - onlyTitle?: boolean; -} - -export type MapSearchIn = SearchIn; - -export type MapSearchOut = SearchResult; +export type MapSearchIn = MapCrudTypes['SearchIn']; +export type MapSearchOut = MapCrudTypes['SearchOut']; +export type MapSearchOptions = MapCrudTypes['SearchOptions']; diff --git a/x-pack/plugins/maps/public/content_management/maps_client.ts b/x-pack/plugins/maps/public/content_management/maps_client.ts index 932765899da22..065d44fdc0681 100644 --- a/x-pack/plugins/maps/public/content_management/maps_client.ts +++ b/x-pack/plugins/maps/public/content_management/maps_client.ts @@ -19,18 +19,19 @@ import type { MapSearchOut, MapSearchOptions, } from '../../common/content_management'; +import { CONTENT_ID as contentTypeId } from '../../common/content_management'; import { getContentManagement } from '../kibana_services'; const get = async (id: string) => { return getContentManagement().client.get({ - contentTypeId: 'map', + contentTypeId, id, }); }; const create = async ({ data, options }: Omit) => { const res = await getContentManagement().client.create({ - contentTypeId: 'map', + contentTypeId, data, options, }); @@ -39,7 +40,7 @@ const create = async ({ data, options }: Omit) => const update = async ({ id, data, options }: Omit) => { const res = await getContentManagement().client.update({ - contentTypeId: 'map', + contentTypeId, id, data, options, @@ -49,14 +50,14 @@ const update = async ({ id, data, options }: Omit) const deleteMap = async (id: string) => { await getContentManagement().client.delete({ - contentTypeId: 'map', + contentTypeId, id, }); }; const search = async (query: SearchQuery = {}, options?: MapSearchOptions) => { return getContentManagement().client.search({ - contentTypeId: 'map', + contentTypeId, query, options, }); diff --git a/x-pack/plugins/maps/server/content_management/maps_storage.ts b/x-pack/plugins/maps/server/content_management/maps_storage.ts index 5a031c675a32c..3063980a561f3 100644 --- a/x-pack/plugins/maps/server/content_management/maps_storage.ts +++ b/x-pack/plugins/maps/server/content_management/maps_storage.ts @@ -28,10 +28,10 @@ import type { MapGetOut, MapCreateIn, MapCreateOut, - CreateOptions, + MapCreateOptions, MapUpdateIn, MapUpdateOut, - UpdateOptions, + MapUpdateOptions, MapDeleteOut, MapSearchOptions, MapSearchOut, @@ -140,7 +140,7 @@ export class MapsStorage async create( ctx: StorageContext, data: MapCreateIn['data'], - options: CreateOptions + options: MapCreateOptions ): Promise { const { utils: { getTransforms }, @@ -157,8 +157,8 @@ export class MapsStorage } const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up< - CreateOptions, - CreateOptions + MapCreateOptions, + MapCreateOptions >(options); if (optionsError) { throw Boom.badRequest(`Invalid options. ${optionsError.message}`); @@ -191,7 +191,7 @@ export class MapsStorage ctx: StorageContext, id: string, data: MapUpdateIn['data'], - options: UpdateOptions + options: MapUpdateOptions ): Promise { const { utils: { getTransforms }, @@ -208,8 +208,8 @@ export class MapsStorage } const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up< - CreateOptions, - CreateOptions + MapCreateOptions, + MapCreateOptions >(options); if (optionsError) { throw Boom.badRequest(`Invalid options. ${optionsError.message}`); diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 9e501ba66559c..398139fe00d34 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -67,6 +67,7 @@ "@kbn/core-saved-objects-api-server", "@kbn/object-versioning", "@kbn/field-types", + "@kbn/content-management-utils", ], "exclude": [ "target/**/*", diff --git a/yarn.lock b/yarn.lock index d120721650856..bc57b5fc7d283 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3077,6 +3077,10 @@ version "0.0.0" uid "" +"@kbn/content-management-utils@link:packages/kbn-content-management-utils": + version "0.0.0" + uid "" + "@kbn/controls-example-plugin@link:examples/controls_example": version "0.0.0" uid "" From 67322fe2a7f8f6932f3b26a84000c4bf9c8cc1d9 Mon Sep 17 00:00:00 2001 From: Andrew Macri Date: Mon, 24 Apr 2023 20:27:58 -0600 Subject: [PATCH 32/36] [Security Solution] Data Quality dashboard storage metrics (#155581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [Security Solution] Data Quality dashboard storage metrics ![storage_metrics_animated](https://user-images.githubusercontent.com/4459398/233871314-6894b380-63ac-4622-b64f-965752a96019.gif) _Above: The new storage metrics treemap updates as indices are checked_ ![storage_metrics](https://user-images.githubusercontent.com/4459398/233880225-8242733a-4bd6-40b3-bffa-e283ce0d77cd.png) _Above: Storage metrics in the Data Quality dashboard_ ## Summary This PR introduces [storage metrics](https://github.com/elastic/security-team/issues/6047) to the _Data Quality_ dashboard - Multiple views are enhanced to display the size of indices - A new interactive treemap visualizes the relative sizes of indices - Markdown reports include the size of indices - The Data Quality dashboard `Beta` tag is removed - Inline action buttons replace the `Take action` popover - The Global stats panel remains visible when the `Select one or more ILM phases` help is displayed - Code coverage is improved throughout the dashboard ## Details ### Multiple views enhanced to display the size of indices The following views have been enhanced to display the `Size` of indices, per the screenshots below: - The pattern table's `Size` column displays the size of a single index ![04_size_column](https://user-images.githubusercontent.com/4459398/233870161-d86eadbd-9f01-4ed6-aa6f-98f6044a4f57.png) - The pattern table's `Size` tooltip ![05_size_column_tooltip](https://user-images.githubusercontent.com/4459398/233868732-08059ba9-5e4b-4f68-a152-eb4b41db6f96.png) - The pattern rollup's `Size` stat displays the total size of indices in a pattern ![06_pattern_rollups_size](https://user-images.githubusercontent.com/4459398/233868817-babc96eb-c0aa-4b7f-bb45-54e3039d06f2.png) - The pattern rollup's `Size` stat tooltip ![07_pattern_rollups_size_tooltip](https://user-images.githubusercontent.com/4459398/233868858-14a43aa2-324f-40bd-a185-1cb7ac15c81b.png) - The global stats rollup `Size` stat displays the total size of all the patterns ![08_global_stats_rollup_size](https://user-images.githubusercontent.com/4459398/233868900-e3cbc00b-3b5a-4756-8246-cb31a1b8bac8.png) - The global stats rollup `Size` stat tooltip ![09_global_stats_rollup_size_tooltip](https://user-images.githubusercontent.com/4459398/233868952-b9c27432-c8a4-4ad5-9dda-5e1aa903758c.png) ### New interactive treemap A new interactive treemap visualizes the relative sizes of indices: - The color of indices in the treemap and its legend update as the data is checked ![storage_metrics_animated](https://user-images.githubusercontent.com/4459398/233871314-6894b380-63ac-4622-b64f-965752a96019.gif) - Clicking on an index in the treemap or the legend expands (and scrolls to) the index ### Markdown reports include the `Size` of indices Markdown reports are enhanced to include the new `Size` statistic in: - Pattern markdown tables | Result | Index | Docs | Incompatible fields | ILM Phase | Size | |--------|-------|------|---------------------|-----------|------| | ❌ | auditbeat-7.14.2-2023.04.09-000001 | 48,077 (4.3%) | 12 | `hot` | 41.3MB | | ❌ | auditbeat-7.3.2-2023.04.09-000001 | 48,068 (4.3%) | 8 | `hot` | 31MB | | ❌ | auditbeat-7.11.2-2023.04.09-000001 | 48,064 (4.3%) | 12 | `hot` | 40.8MB | - Pattern rollup markdown tables | Incompatible fields | Indices checked | Indices | Size | Docs | |---------------------|-----------------|---------|------|------| | 164 | 26 | 26 | 899.3MB | 1,118,155 | - The global stats markdown table | Incompatible fields | Indices checked | Indices | Size | Docs | |---------------------|-----------------|---------|------|------| | 166 | 32 | 32 | 9.2GB | 20,779,245 | ### Data Quality dashboard `Beta` tag removed The Data Quality dashboard `Beta` tag is removed from the following views: - The `Dashboards` page **Before:** ![11_dashboards_before](https://user-images.githubusercontent.com/4459398/233869434-d4d2ed14-4e6f-4eab-bae6-a9c9b976e20f.png) **After:** ![12_dashboards_after](https://user-images.githubusercontent.com/4459398/233869088-9dc62d7d-44cb-46cb-8880-976a7b7e9c56.png) - Security Solution side navigation **Before:** ![13_side_navigation_before](https://user-images.githubusercontent.com/4459398/233869467-e7725285-1199-40e1-ac65-054bea8b02f6.png) **After:** ![14_side_navigation_after](https://user-images.githubusercontent.com/4459398/233869146-7b89cb47-3509-478e-8675-9f1653749b18.png) - The Data Quality dashboard page header **Before:** ![15_page_header_before](https://user-images.githubusercontent.com/4459398/233869404-0b04c2ec-3d2e-4ba8-9520-68013f80e43a.png) **After:** ![16_page_header_after](https://user-images.githubusercontent.com/4459398/233869219-b54ee61e-07b7-470d-a668-b4f5ed4327e6.png) ### Inline action buttons replace the `Take action` popover Inline `Add to new case` and `Copy to clipboard` action buttons replace the `Take action` popover, the previous home of these actions: **Before:** ![17_actions_before](https://user-images.githubusercontent.com/4459398/233869306-0182145f-affc-4ad1-b63f-72e43d34234c.png) **After:** ![18_actions_after](https://user-images.githubusercontent.com/4459398/233869345-754b7448-9d28-4253-9186-5b2389acf4ff.png) ### Global stats panel remains visible when the `Select one or more ILM phases` help is displayed The Global stats panel now remains visible when the `Select one or more ILM phases` help is displayed: **Before:** ![19_select_ilm_phases_before](https://user-images.githubusercontent.com/4459398/233869754-2067fa5d-7153-407b-aa45-65332b16bc7a.png) **After:** ![20_select_ilm_phases_after](https://user-images.githubusercontent.com/4459398/233869762-38d069de-3191-4e28-8692-df42ab3b21a5.png) ### Code coverage improvements Code coverage is improved throughout the dashboard, as measured by running the following command: ```sh node scripts/jest --watch x-pack/packages/kbn-ecs-data-quality-dashboard --coverage ``` --- .../ecs_allowed_values/index.test.tsx | 53 ++ .../{helpers.test.tsx => index.test.tsx} | 51 +- .../{helpers.test.tsx => index.test.tsx} | 4 +- .../compare_fields_table/helpers.test.tsx | 414 +++++++++ .../compare_fields_table/helpers.tsx | 20 +- .../compare_fields_table/index.test.tsx | 39 + .../compare_fields_table/index.tsx | 3 +- .../index_invalid_values/index.test.tsx | 49 + .../index_invalid_values/index.tsx | 2 +- .../allowed_values/helpers.test.tsx | 207 +++++ .../body/data_quality_details/index.test.tsx | 111 +++ .../body/data_quality_details/index.tsx | 123 +++ .../indices_details/index.test.tsx | 93 ++ .../indices_details/index.tsx | 110 +++ .../storage_details/helpers.test.ts | 382 ++++++++ .../storage_details/helpers.ts | 223 +++++ .../storage_details/index.test.tsx | 59 ++ .../storage_details/index.tsx | 58 ++ .../data_quality_details}/translations.ts | 13 +- .../data_quality_panel/body/index.test.tsx | 100 ++ .../data_quality_panel/body/index.tsx | 49 +- .../check_status/index.test.tsx | 212 +++++ .../check_status/index.tsx | 21 +- .../errors_popover/index.test.tsx | 97 ++ .../errors_popover/index.tsx | 12 +- .../errors_viewer/helpers.test.tsx | 122 +++ .../errors_viewer/helpers.tsx | 9 +- .../errors_viewer/index.test.tsx | 77 ++ .../errors_viewer/index.tsx | 2 +- .../data_quality_summary/index.test.tsx | 103 +++ .../data_quality_summary/index.tsx | 56 +- .../summary_actions/actions/index.test.tsx | 104 +++ .../summary_actions/actions/index.tsx | 98 ++ .../check_all/check_index.test.ts | 338 +++++++ .../summary_actions/check_all/check_index.ts | 11 +- .../summary_actions/check_all/helpers.test.ts | 107 +++ .../summary_actions/check_all/helpers.ts | 8 +- .../summary_actions/check_all/index.test.tsx | 377 ++++++++ .../summary_actions/check_all/index.tsx | 28 +- .../summary_actions/index.test.tsx | 128 +++ .../summary_actions/index.tsx | 122 ++- .../take_action_menu/index.tsx | 125 --- .../error_empty_prompt/index.test.tsx | 26 + .../error_empty_prompt/index.tsx | 2 +- .../ilm_phase_counts/index.test.tsx | 86 ++ .../ilm_phase_counts/index.tsx | 2 +- .../empty_prompt_body.test.tsx | 26 + .../index_properties/empty_prompt_body.tsx | 4 +- .../empty_prompt_title.test.tsx | 26 + .../index_properties/empty_prompt_title.tsx | 4 +- .../index_properties/helpers.test.ts | 800 ++++++++++++++++ .../index_properties/index.test.tsx | 262 ++++++ .../index_properties/index.tsx | 31 +- .../index_properties/markdown/helpers.test.ts | 598 +++++++++++- .../index_properties/markdown/helpers.ts | 85 +- .../loading_empty_prompt/index.tsx | 4 +- .../panel_subtitle/index.tsx | 34 - .../pattern/helpers.test.ts | 865 ++++++++++++++++++ .../data_quality_panel/pattern/helpers.ts | 85 +- .../data_quality_panel/pattern/index.test.tsx | 17 +- .../data_quality_panel/pattern/index.tsx | 107 ++- .../pattern/pattern_summary/index.tsx | 12 +- .../pattern_label/helpers.test.ts | 97 ++ .../pattern_summary/stats_rollup/index.tsx | 51 +- .../pattern/translations.ts | 2 +- .../same_family/index.test.tsx | 2 +- .../stat_label/translations.ts | 18 + .../storage_treemap/index.test.tsx | 154 ++++ .../storage_treemap/index.tsx | 201 ++++ .../storage_treemap/no_data/index.test.tsx | 21 + .../storage_treemap/no_data/index.tsx | 43 + .../storage_treemap/translations.ts | 20 + .../summary_table/helpers.test.tsx | 543 +++++++++++ .../summary_table/helpers.tsx | 56 +- .../summary_table/index.test.tsx | 89 ++ .../summary_table/index.tsx | 91 +- .../summary_table/translations.ts | 4 + .../callouts/custom_callout/index.test.tsx | 7 +- .../incompatible_callout/helpers.test.ts | 2 +- .../incompatible_callout/index.test.tsx | 4 +- .../tabs/custom_tab/helpers.test.ts | 73 +- .../tabs/custom_tab/helpers.ts | 17 +- .../tabs/custom_tab/index.tsx | 28 +- .../data_quality_panel/tabs/helpers.test.tsx | 112 +++ .../data_quality_panel/tabs/helpers.tsx | 24 +- .../tabs/incompatible_tab/helpers.test.ts | 317 ++++++- .../tabs/incompatible_tab/helpers.ts | 17 +- .../tabs/incompatible_tab/index.tsx | 32 +- .../data_quality_panel/tabs/styles.tsx | 32 +- .../summary_tab/callout_summary/index.tsx | 19 +- .../tabs/summary_tab/helpers.test.ts | 235 +++++ .../tabs/summary_tab/helpers.ts | 6 + .../tabs/summary_tab/index.tsx | 12 +- .../chart_legend/chart_legend_item.tsx | 58 +- .../chart_legend/index.tsx | 53 +- .../ecs_summary_donut_chart/helpers.test.ts | 29 + .../impl/data_quality/helpers.test.ts | 218 +++-- .../impl/data_quality/helpers.ts | 20 + .../ilm_phases_empty_prompt/index.test.tsx | 28 + .../impl/data_quality/index.test.tsx | 30 +- .../impl/data_quality/index.tsx | 24 +- .../allowed_values/mock_allowed_values.ts | 128 +++ .../data_quality_check_result/mock_index.tsx | 41 + ...dex.ts => mock_enriched_field_metadata.ts} | 72 ++ .../{index.ts => mock_ilm_explain.ts} | 4 + ...ndices_get_mapping_index_mapping_record.ts | 73 ++ .../mock_mappings_properties.ts | 107 +++ .../mock_mappings_response.ts | 68 ++ .../mock_alerts_pattern_rollup.ts | 2 + .../mock_auditbeat_pattern_rollup.ts | 38 + .../mock_packetbeat_pattern_rollup.ts | 636 +++++++++++++ .../data_quality/mock/stats/mock_stats.tsx | 275 ++++++ .../{index.tsx => test_providers.tsx} | 0 .../unallowed_values/mock_unallowed_values.ts | 122 +++ .../impl/data_quality/styles.tsx | 9 +- .../impl/data_quality/translations.ts | 25 +- .../impl/data_quality/types.ts | 16 + .../data_quality/use_mappings/helpers.test.ts | 88 ++ .../use_results_rollup/helpers.test.ts | 511 +++++++++++ .../use_results_rollup/helpers.ts | 32 +- .../data_quality/use_results_rollup/index.tsx | 7 + .../use_unallowed_values/helpers.test.ts | 503 ++++++++++ .../use_unallowed_values/helpers.ts | 1 + .../public/overview/links.ts | 1 - .../public/overview/pages/data_quality.tsx | 16 +- .../translations/translations/fr-FR.json | 5 - .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 128 files changed, 12119 insertions(+), 856 deletions(-) create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/{helpers.test.tsx => index.test.tsx} (87%) rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/{helpers.test.tsx => index.test.tsx} (97%) create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/allowed_values/helpers.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/{data_quality_summary/summary_actions/take_action_menu => body/data_quality_details}/translations.ts (51%) create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx delete mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx delete mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/panel_subtitle/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/translations.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/allowed_values/mock_allowed_values.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/data_quality_check_result/mock_index.tsx rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/{index.ts => mock_enriched_field_metadata.ts} (87%) rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/{index.ts => mock_ilm_explain.ts} (94%) create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_properties/mock_mappings_properties.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_response/mock_mappings_response.ts rename x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/test_providers/{index.tsx => test_providers.tsx} (100%) create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/unallowed_values/mock_unallowed_values.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_mappings/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.test.ts create mode 100644 x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.test.ts diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx new file mode 100644 index 0000000000000..07af65877da01 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/ecs_allowed_values/index.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { mockAllowedValues } from '../../mock/allowed_values/mock_allowed_values'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { EcsAllowedValues } from '.'; + +describe('EcsAllowedValues', () => { + describe('when `allowedValues` exists', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it renders the allowed values', () => { + expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( + mockAllowedValues.map(({ name }) => `${name}`).join('') + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); + }); + }); + + describe('when `allowedValues` is undefined', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it does NOT render the allowed values', () => { + expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx similarity index 87% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/helpers.test.tsx rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx index 75a34735abff1..e3e68152f4c06 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/helpers.test.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_common_table_columns/index.test.tsx @@ -10,57 +10,57 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations'; -import { TestProviders } from '../../mock/test_providers'; import { eventCategory, eventCategoryWithUnallowedValues, -} from '../../mock/enriched_field_metadata'; +} from '../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { + DOCUMENT_VALUES_ACTUAL, + ECS_DESCRIPTION, + ECS_MAPPING_TYPE_EXPECTED, + ECS_VALUES_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, +} from '../translations'; import { EnrichedFieldMetadata } from '../../types'; import { EMPTY_PLACEHOLDER, getCommonTableColumns } from '.'; describe('getCommonTableColumns', () => { test('it returns the expected column configuration', () => { - const columns = getCommonTableColumns().map((x) => omit('render', x)); - - expect(columns).toEqual([ - { - field: 'indexFieldName', - name: 'Field', - sortable: true, - truncateText: false, - width: '20%', - }, + expect(getCommonTableColumns().map((x) => omit('render', x))).toEqual([ + { field: 'indexFieldName', name: FIELD, sortable: true, truncateText: false, width: '20%' }, { field: 'type', - name: 'ECS mapping type (expected)', + name: ECS_MAPPING_TYPE_EXPECTED, sortable: true, truncateText: false, width: '15%', }, { field: 'indexFieldType', - name: 'Index mapping type (actual)', + name: INDEX_MAPPING_TYPE_ACTUAL, sortable: true, truncateText: false, width: '15%', }, { field: 'allowed_values', - name: 'ECS values (expected)', + name: ECS_VALUES_EXPECTED, sortable: false, truncateText: false, width: '15%', }, { field: 'indexInvalidValues', - name: 'Document values (actual)', + name: DOCUMENT_VALUES_ACTUAL, sortable: false, truncateText: false, width: '15%', }, { field: 'description', - name: 'ECS description', + name: ECS_DESCRIPTION, sortable: false, truncateText: false, width: '20%', @@ -141,7 +141,7 @@ describe('getCommonTableColumns', () => { const withTypeMismatchDifferentFamily: EnrichedFieldMetadata = { ...eventCategory, // `event.category` is a `keyword` per the ECS spec indexFieldType, // this index has a mapping of `text` instead of `keyword` - isInSameFamily: false, // `text` and `keyword` are not in the same family + isInSameFamily: false, // `text` and `wildcard` are not in the same family }; render( @@ -159,29 +159,18 @@ describe('getCommonTableColumns', () => { }); describe('when the index field matches the ECS type', () => { - const indexFieldType = 'keyword'; - test('it renders the expected type with success styling', () => { const columns = getCommonTableColumns(); const indexFieldTypeColumnRender = columns[2].render; - const withTypeMismatchDifferentFamily: EnrichedFieldMetadata = { - ...eventCategory, // `event.category` is a `keyword` per the ECS spec - indexFieldType, // exactly matches the ECS spec - isInSameFamily: true, // `keyword` is a member of the `keyword` family - }; - render( {indexFieldTypeColumnRender != null && - indexFieldTypeColumnRender( - withTypeMismatchDifferentFamily.indexFieldType, - withTypeMismatchDifferentFamily - )} + indexFieldTypeColumnRender(eventCategory.indexFieldType, eventCategory)} ); - expect(screen.getByTestId('codeSuccess')).toHaveTextContent(indexFieldType); + expect(screen.getByTestId('codeSuccess')).toHaveTextContent(eventCategory.indexFieldType); }); }); }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx similarity index 97% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/helpers.test.tsx rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx index 160b300e08934..132e8b2fc302b 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/helpers.test.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/get_incompatible_mappings_table_columns/index.test.tsx @@ -10,8 +10,8 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { SAME_FAMILY } from '../../data_quality_panel/same_family/translations'; -import { TestProviders } from '../../mock/test_providers'; -import { eventCategory } from '../../mock/enriched_field_metadata'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { eventCategory } from '../../mock/enriched_field_metadata/mock_enriched_field_metadata'; import { EnrichedFieldMetadata } from '../../types'; import { EMPTY_PLACEHOLDER, getIncompatibleMappingsTableColumns } from '.'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx new file mode 100644 index 0000000000000..7c72289290942 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.test.tsx @@ -0,0 +1,414 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { omit } from 'lodash/fp'; +import React from 'react'; + +import { + EMPTY_PLACEHOLDER, + getCustomTableColumns, + getEcsCompliantTableColumns, + getIncompatibleValuesTableColumns, +} from './helpers'; +import { + eventCategory, + eventCategoryWithUnallowedValues, + someField, +} from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { TestProviders } from '../mock/test_providers/test_providers'; + +describe('helpers', () => { + describe('getCustomTableColumns', () => { + test('it returns the expected columns', () => { + expect(getCustomTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '50%', + }, + { + field: 'indexFieldType', + name: 'Index mapping type', + sortable: true, + truncateText: false, + width: '50%', + }, + ]); + }); + + describe('indexFieldType render()', () => { + test('it renders the indexFieldType', () => { + const columns = getCustomTableColumns(); + const indexFieldTypeRender = columns[1].render; + + render( + + <> + {indexFieldTypeRender != null && + indexFieldTypeRender(someField.indexFieldType, someField)} + + + ); + + expect(screen.getByTestId('indexFieldType')).toHaveTextContent(someField.indexFieldType); + }); + }); + }); + + describe('getEcsCompliantTableColumns', () => { + test('it returns the expected columns', () => { + expect(getEcsCompliantTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'type', + name: 'ECS mapping type', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'allowed_values', + name: 'ECS values', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '25%', + }, + ]); + }); + + describe('type render()', () => { + describe('when `type` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const typeRender = columns[1].render; + + render( + + <>{typeRender != null && typeRender(eventCategory.type, eventCategory)} + + ); + }); + + test('it renders the expected `type`', () => { + expect(screen.getByTestId('type')).toHaveTextContent('keyword'); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('typePlaceholder')).not.toBeInTheDocument(); + }); + }); + + describe('when `type` is undefined', () => { + beforeEach(() => { + const withUndefinedType = { + ...eventCategory, + type: undefined, // <-- + }; + const columns = getEcsCompliantTableColumns(); + const typeRender = columns[1].render; + + render( + + <>{typeRender != null && typeRender(withUndefinedType.type, withUndefinedType)} + + ); + }); + + test('it does NOT render the `type`', () => { + expect(screen.queryByTestId('type')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('typePlaceholder')).toHaveTextContent(EMPTY_PLACEHOLDER); + }); + }); + }); + + describe('allowed values render()', () => { + describe('when `allowedValues` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const allowedValuesRender = columns[2].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender(eventCategory.allowed_values, eventCategory)} + + + ); + }); + + test('it renders the expected `AllowedValue` names', () => { + expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( + eventCategory.allowed_values?.map(({ name }) => name).join('') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); + }); + }); + + describe('when `allowedValues` is undefined', () => { + const withUndefinedAllowedValues = { + ...eventCategory, + allowed_values: undefined, // <-- + }; + + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const allowedValuesRender = columns[2].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender( + withUndefinedAllowedValues.allowed_values, + withUndefinedAllowedValues + )} + + + ); + }); + + test('it does NOT render the `AllowedValue` names', () => { + expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); + }); + }); + }); + + describe('description render()', () => { + describe('when `description` exists', () => { + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const descriptionRender = columns[3].render; + + render( + + <> + {descriptionRender != null && + descriptionRender(eventCategory.description, eventCategory)} + + + ); + }); + + test('it renders the expected `description`', () => { + expect(screen.getByTestId('description')).toHaveTextContent( + eventCategory.description?.replaceAll('\n', ' ') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); + }); + }); + + describe('when `description` is undefined', () => { + const withUndefinedDescription = { + ...eventCategory, + description: undefined, // <-- + }; + + beforeEach(() => { + const columns = getEcsCompliantTableColumns(); + const descriptionRender = columns[3].render; + + render( + + <> + {descriptionRender != null && + descriptionRender(withUndefinedDescription.description, withUndefinedDescription)} + + + ); + }); + + test('it does NOT render the `description`', () => { + expect(screen.queryByTestId('description')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument(); + }); + }); + }); + }); + + describe('getIncompatibleValuesTableColumns', () => { + test('it returns the expected columns', () => { + expect(getIncompatibleValuesTableColumns().map((x) => omit('render', x))).toEqual([ + { + field: 'indexFieldName', + name: 'Field', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'allowed_values', + name: 'ECS values (expected)', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'indexInvalidValues', + name: 'Document values (actual)', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'description', + name: 'ECS description', + sortable: false, + truncateText: false, + width: '25%', + }, + ]); + }); + + describe('allowed values render()', () => { + describe('when `allowedValues` exists', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const allowedValuesRender = columns[1].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender(eventCategory.allowed_values, eventCategory)} + + + ); + }); + + test('it renders the expected `AllowedValue` names', () => { + expect(screen.getByTestId('ecsAllowedValues')).toHaveTextContent( + eventCategory.allowed_values?.map(({ name }) => name).join('') ?? '' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('ecsAllowedValuesEmpty')).not.toBeInTheDocument(); + }); + }); + + describe('when `allowedValues` is undefined', () => { + const withUndefinedAllowedValues = { + ...eventCategory, + allowed_values: undefined, // <-- + }; + + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const allowedValuesRender = columns[1].render; + + render( + + <> + {allowedValuesRender != null && + allowedValuesRender( + withUndefinedAllowedValues.allowed_values, + withUndefinedAllowedValues + )} + + + ); + }); + + test('it does NOT render the `AllowedValue` names', () => { + expect(screen.queryByTestId('ecsAllowedValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('ecsAllowedValuesEmpty')).toBeInTheDocument(); + }); + }); + }); + + describe('indexInvalidValues render()', () => { + describe('when `indexInvalidValues` is populated', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const indexInvalidValuesRender = columns[2].render; + + render( + + <> + {indexInvalidValuesRender != null && + indexInvalidValuesRender( + eventCategoryWithUnallowedValues.indexInvalidValues, + eventCategoryWithUnallowedValues + )} + + + ); + }); + + test('it renders the expected `indexInvalidValues`', () => { + expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( + 'an_invalid_category (2)theory (1)' + ); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); + }); + }); + + describe('when `indexInvalidValues` is empty', () => { + beforeEach(() => { + const columns = getIncompatibleValuesTableColumns(); + const indexInvalidValuesRender = columns[2].render; + + render( + + <> + {indexInvalidValuesRender != null && + indexInvalidValuesRender(eventCategory.indexInvalidValues, eventCategory)} + + + ); + }); + + test('it does NOT render the index invalid values', () => { + expect(screen.queryByTestId('indexInvalidValues')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument(); + }); + }); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.tsx index e9a85c2908b89..8153380c140c3 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/helpers.tsx @@ -30,7 +30,9 @@ export const getCustomTableColumns = (): Array< { field: 'indexFieldType', name: i18n.INDEX_MAPPING_TYPE, - render: (indexFieldType: string) => {indexFieldType}, + render: (indexFieldType: string) => ( + {indexFieldType} + ), sortable: true, truncateText: false, width: '50%', @@ -50,8 +52,12 @@ export const getEcsCompliantTableColumns = (): Array< { field: 'type', name: i18n.ECS_MAPPING_TYPE, - render: (type: string) => - type != null ? {type} : {EMPTY_PLACEHOLDER}, + render: (type: string | undefined) => + type != null ? ( + {type} + ) : ( + {EMPTY_PLACEHOLDER} + ), sortable: true, truncateText: false, width: '25%', @@ -69,8 +75,12 @@ export const getEcsCompliantTableColumns = (): Array< { field: 'description', name: i18n.ECS_DESCRIPTION, - render: (description: string) => - description != null ? description : {EMPTY_PLACEHOLDER}, + render: (description: string | undefined) => + description != null ? ( + {description} + ) : ( + {EMPTY_PLACEHOLDER} + ), sortable: false, truncateText: false, width: '25%', diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.test.tsx new file mode 100644 index 0000000000000..8732f27702bc2 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../data_quality_panel/tabs/incompatible_tab/translations'; +import { eventCategory } from '../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { TestProviders } from '../mock/test_providers/test_providers'; +import { CompareFieldsTable } from '.'; +import { getIncompatibleMappingsTableColumns } from './get_incompatible_mappings_table_columns'; + +describe('CompareFieldsTable', () => { + describe('rendering', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it renders the expected title', () => { + expect(screen.getByTestId('title')).toHaveTextContent('Incompatible field mappings - foo'); + }); + + test('it renders the table', () => { + expect(screen.getByTestId('table')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.tsx index 2efc9355c710f..145686785cafa 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index.tsx @@ -36,11 +36,12 @@ const CompareFieldsTableComponent: React.FC = ({ return ( <> - <>{title} + {title} { + test('it renders a placeholder with the expected content when `indexInvalidValues` is empty', () => { + render( + + + + ); + + expect(screen.getByTestId('emptyPlaceholder')).toHaveTextContent(EMPTY_PLACEHOLDER); + }); + + test('it renders the expected field names and counts when the index has invalid values', () => { + const indexInvalidValues: UnallowedValueCount[] = [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ]; + + render( + + + + ); + + expect(screen.getByTestId('indexInvalidValues')).toHaveTextContent( + 'an_invalid_category (2)theory (1)' + ); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.tsx index d3df809215d08..2b58ea98b8b28 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/compare_fields_table/index_invalid_values/index.tsx @@ -23,7 +23,7 @@ interface Props { const IndexInvalidValuesComponent: React.FC = ({ indexInvalidValues }) => indexInvalidValues.length === 0 ? ( - {EMPTY_PLACEHOLDER} + {EMPTY_PLACEHOLDER} ) : ( {indexInvalidValues.map(({ fieldName, count }, i) => ( diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/allowed_values/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/allowed_values/helpers.test.tsx new file mode 100644 index 0000000000000..7dde4254708b7 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/allowed_values/helpers.test.tsx @@ -0,0 +1,207 @@ +/* + * 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 { EcsFlat } from '@kbn/ecs'; +import { omit } from 'lodash/fp'; + +import { getUnallowedValueRequestItems, getValidValues, hasAllowedValues } from './helpers'; +import { AllowedValue, EcsMetadata } from '../../types'; + +const ecsMetadata: Record = EcsFlat as unknown as Record; + +describe('helpers', () => { + describe('hasAllowedValues', () => { + test('it returns true for a field that has `allowed_values`', () => { + expect( + hasAllowedValues({ + ecsMetadata, + fieldName: 'event.category', + }) + ).toBe(true); + }); + + test('it returns false for a field that does NOT have `allowed_values`', () => { + expect( + hasAllowedValues({ + ecsMetadata, + fieldName: 'host.name', + }) + ).toBe(false); + }); + + test('it returns false for a field that does NOT exist in `ecsMetadata`', () => { + expect( + hasAllowedValues({ + ecsMetadata, + fieldName: 'does.NOT.exist', + }) + ).toBe(false); + }); + + test('it returns false when `ecsMetadata` is null', () => { + expect( + hasAllowedValues({ + ecsMetadata: null, // <-- + fieldName: 'event.category', + }) + ).toBe(false); + }); + }); + + describe('getValidValues', () => { + test('it returns the expected valid values', () => { + expect(getValidValues(ecsMetadata['event.category'])).toEqual([ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ]); + }); + + test('it returns an empty array when the `field` does NOT have `allowed_values`', () => { + expect(getValidValues(ecsMetadata['host.name'])).toEqual([]); + }); + + test('it returns an empty array when `field` is undefined', () => { + expect(getValidValues(undefined)).toEqual([]); + }); + + test('it skips `allowed_values` where `name` is undefined', () => { + // omit the `name` property from the `database` `AllowedValue`: + const missingDatabase = + ecsMetadata['event.category'].allowed_values?.map((x) => + x.name === 'database' ? omit('name', x) : x + ) ?? []; + + const field = { + ...ecsMetadata['event.category'], + allowed_values: missingDatabase, + }; + + expect(getValidValues(field)).toEqual([ + 'authentication', + 'configuration', + // no entry for 'database' + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ]); + }); + }); + + describe('getUnallowedValueRequestItems', () => { + test('it returns the expected request items', () => { + expect( + getUnallowedValueRequestItems({ + ecsMetadata, + indexName: 'auditbeat-*', + }) + ).toEqual([ + { + indexName: 'auditbeat-*', + indexFieldName: 'event.category', + allowedValues: [ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + }, + { + indexName: 'auditbeat-*', + indexFieldName: 'event.kind', + allowedValues: [ + 'alert', + 'enrichment', + 'event', + 'metric', + 'state', + 'pipeline_error', + 'signal', + ], + }, + { + indexName: 'auditbeat-*', + indexFieldName: 'event.outcome', + allowedValues: ['failure', 'success', 'unknown'], + }, + { + indexName: 'auditbeat-*', + indexFieldName: 'event.type', + allowedValues: [ + 'access', + 'admin', + 'allowed', + 'change', + 'connection', + 'creation', + 'deletion', + 'denied', + 'end', + 'error', + 'group', + 'indicator', + 'info', + 'installation', + 'protocol', + 'start', + 'user', + ], + }, + ]); + }); + + test('it returns an empty array when `ecsMetadata` is null', () => { + expect( + getUnallowedValueRequestItems({ + ecsMetadata: null, // <-- + indexName: 'auditbeat-*', + }) + ).toEqual([]); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx new file mode 100644 index 0000000000000..8b7c9b01e3c5e --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.test.tsx @@ -0,0 +1,111 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../../helpers'; +import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { PatternRollup } from '../../../types'; +import { Props, DataQualityDetails } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const patternIndexNames: Record = { + 'auditbeat-*': [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-custom-index-1', + ], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], + 'packetbeat-*': [ + '.ds-packetbeat-8.5.3-2023.02.04-000001', + '.ds-packetbeat-8.6.1-2023.02.04-000001', + ], +}; + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: jest.fn(), + formatBytes, + formatNumber, + getGroupByFieldsOnClick: jest.fn(), + ilmPhases, + openCreateCaseFlyout: jest.fn(), + patternIndexNames, + patternRollups, + patterns, + theme: DARK_THEME, + updatePatternIndexNames: jest.fn(), + updatePatternRollup: jest.fn(), +}; + +describe('DataQualityDetails', () => { + describe('when ILM phases are provided', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the storage details', () => { + expect(screen.getByTestId('storageDetails')).toBeInTheDocument(); + }); + + test('it renders the indices details', () => { + expect(screen.getByTestId('indicesDetails')).toBeInTheDocument(); + }); + }); + + describe('when ILM phases are are empty', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders an empty prompt when ilmPhases is empty', () => { + expect(screen.getByTestId('ilmPhasesEmptyPrompt')).toBeInTheDocument(); + }); + + test('it does NOT render the storage details', () => { + expect(screen.queryByTestId('storageDetails')).not.toBeInTheDocument(); + }); + + test('it does NOT render the indices details', () => { + expect(screen.queryByTestId('indicesDetails')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx new file mode 100644 index 0000000000000..3c996dd095dc8 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/index.tsx @@ -0,0 +1,123 @@ +/* + * 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 type { + FlameElementEvent, + HeatmapElementEvent, + MetricElementEvent, + PartitionElementEvent, + Theme, + WordCloudElementEvent, + XYChartElementEvent, +} from '@elastic/charts'; + +import React, { useCallback, useState } from 'react'; + +import { IlmPhasesEmptyPrompt } from '../../../ilm_phases_empty_prompt'; +import { IndicesDetails } from './indices_details'; +import { StorageDetails } from './storage_details'; +import { PatternRollup, SelectedIndex } from '../../../types'; + +export interface Props { + addSuccessToast: (toast: { title: string }) => void; + canUserCreateAndReadCases: () => boolean; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + getGroupByFieldsOnClick: ( + elements: Array< + | FlameElementEvent + | HeatmapElementEvent + | MetricElementEvent + | PartitionElementEvent + | WordCloudElementEvent + | XYChartElementEvent + > + ) => { + groupByField0: string; + groupByField1: string; + }; + ilmPhases: string[]; + openCreateCaseFlyout: ({ + comments, + headerContent, + }: { + comments: string[]; + headerContent?: React.ReactNode; + }) => void; + patternIndexNames: Record; + patternRollups: Record; + patterns: string[]; + theme: Theme; + updatePatternIndexNames: ({ + indexNames, + pattern, + }: { + indexNames: string[]; + pattern: string; + }) => void; + updatePatternRollup: (patternRollup: PatternRollup) => void; +} + +const DataQualityDetailsComponent: React.FC = ({ + addSuccessToast, + canUserCreateAndReadCases, + formatBytes, + formatNumber, + getGroupByFieldsOnClick, + ilmPhases, + openCreateCaseFlyout, + patternIndexNames, + patternRollups, + patterns, + theme, + updatePatternIndexNames, + updatePatternRollup, +}) => { + const [selectedIndex, setSelectedIndex] = useState(null); + + const onIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => { + setSelectedIndex({ indexName, pattern }); + }, []); + + if (ilmPhases.length === 0) { + return ; + } + + return ( + <> + + + + + ); +}; + +DataQualityDetailsComponent.displayName = 'DataQualityDetailsComponent'; +export const DataQualityDetails = React.memo(DataQualityDetailsComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx new file mode 100644 index 0000000000000..ee4977ebe7858 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../../../helpers'; +import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { PatternRollup } from '../../../../types'; +import { Props, IndicesDetails } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const patternIndexNames: Record = { + 'auditbeat-*': [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-custom-index-1', + ], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], + 'packetbeat-*': [ + '.ds-packetbeat-8.5.3-2023.02.04-000001', + '.ds-packetbeat-8.6.1-2023.02.04-000001', + ], +}; + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: jest.fn(), + formatBytes, + formatNumber, + getGroupByFieldsOnClick: jest.fn(), + ilmPhases, + openCreateCaseFlyout: jest.fn(), + patternIndexNames, + patternRollups, + patterns, + selectedIndex: null, + setSelectedIndex: jest.fn(), + theme: DARK_THEME, + updatePatternIndexNames: jest.fn(), + updatePatternRollup: jest.fn(), +}; + +describe('IndicesDetails', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + describe('rendering patterns', () => { + patterns.forEach((pattern) => { + test(`it renders the ${pattern} pattern`, () => { + expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument(); + }); + }); + }); + + describe('rendering spacers', () => { + test('it renders the expected number of spacers', () => { + expect(screen.getAllByTestId('bodyPatternSpacer')).toHaveLength(patterns.length - 1); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx new file mode 100644 index 0000000000000..9b59a78430e1c --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/indices_details/index.tsx @@ -0,0 +1,110 @@ +/* + * 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 type { + FlameElementEvent, + HeatmapElementEvent, + MetricElementEvent, + PartitionElementEvent, + Theme, + WordCloudElementEvent, + XYChartElementEvent, +} from '@elastic/charts'; +import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React from 'react'; + +import { Pattern } from '../../../pattern'; +import { PatternRollup, SelectedIndex } from '../../../../types'; + +export interface Props { + addSuccessToast: (toast: { title: string }) => void; + canUserCreateAndReadCases: () => boolean; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + getGroupByFieldsOnClick: ( + elements: Array< + | FlameElementEvent + | HeatmapElementEvent + | MetricElementEvent + | PartitionElementEvent + | WordCloudElementEvent + | XYChartElementEvent + > + ) => { + groupByField0: string; + groupByField1: string; + }; + ilmPhases: string[]; + openCreateCaseFlyout: ({ + comments, + headerContent, + }: { + comments: string[]; + headerContent?: React.ReactNode; + }) => void; + patternIndexNames: Record; + patternRollups: Record; + patterns: string[]; + selectedIndex: SelectedIndex | null; + setSelectedIndex: (selectedIndex: SelectedIndex | null) => void; + theme: Theme; + updatePatternIndexNames: ({ + indexNames, + pattern, + }: { + indexNames: string[]; + pattern: string; + }) => void; + updatePatternRollup: (patternRollup: PatternRollup) => void; +} + +const IndicesDetailsComponent: React.FC = ({ + addSuccessToast, + canUserCreateAndReadCases, + formatBytes, + formatNumber, + getGroupByFieldsOnClick, + ilmPhases, + openCreateCaseFlyout, + patternIndexNames, + patternRollups, + patterns, + selectedIndex, + setSelectedIndex, + theme, + updatePatternIndexNames, + updatePatternRollup, +}) => ( +
+ {patterns.map((pattern, i) => ( + + + {patterns[i + 1] && } + + ))} +
+); + +IndicesDetailsComponent.displayName = 'IndicesDetailsComponent'; + +export const IndicesDetails = React.memo(IndicesDetailsComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.test.ts new file mode 100644 index 0000000000000..45e8f1ba1b4ad --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.test.ts @@ -0,0 +1,382 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { euiThemeVars } from '@kbn/ui-theme'; + +import { EMPTY_STAT } from '../../../../helpers'; +import { + DEFAULT_INDEX_COLOR, + getFillColor, + getFlattenedBuckets, + getGroupFromPath, + getLayersMultiDimensional, + getLegendItems, + getLegendItemsForPattern, + getPathToFlattenedBucketMap, + getPatternLegendItem, + getPatternSizeInBytes, +} from './helpers'; +import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { PatternRollup } from '../../../../types'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +/** a valid `PatternRollup` that has an undefined `sizeInBytes` */ +const noSizeInBytes: Record = { + 'valid-*': { + docsCount: 19127, + error: null, + ilmExplain: null, + ilmExplainPhaseCounts: { + hot: 1, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 2, + }, + indices: 3, + pattern: 'valid-*', + results: undefined, + sizeInBytes: undefined, // <-- + stats: null, + }, +}; + +describe('helpers', () => { + describe('getPatternSizeInBytes', () => { + test('it returns the expected size when the pattern exists in the rollup', () => { + const pattern = 'auditbeat-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups })).toEqual( + auditbeatWithAllResults.sizeInBytes + ); + }); + + test('it returns zero when the pattern exists in the rollup, but does not have a sizeInBytes', () => { + const pattern = 'valid-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups: noSizeInBytes })).toEqual(0); + }); + + test('it returns zero when the pattern does NOT exist in the rollup', () => { + const pattern = 'does-not-exist-*'; + + expect(getPatternSizeInBytes({ pattern, patternRollups })).toEqual(0); + }); + }); + + describe('getPatternLegendItem', () => { + test('it returns the expected legend item', () => { + const pattern = 'auditbeat-*'; + + expect(getPatternLegendItem({ pattern, patternRollups })).toEqual({ + color: null, + ilmPhase: null, + index: null, + pattern, + sizeInBytes: auditbeatWithAllResults.sizeInBytes, + }); + }); + }); + + describe('getLegendItemsForPattern', () => { + test('it returns the expected legend items', () => { + const pattern = 'auditbeat-*'; + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + patternRollups, + }); + + expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([ + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + }, + ]); + }); + }); + + describe('getLegendItems', () => { + test('it returns the expected legend items', () => { + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + patternRollups, + }); + + expect(getLegendItems({ flattenedBuckets, patterns, patternRollups })).toEqual([ + { + color: null, + ilmPhase: null, + index: null, + pattern: '.alerts-security.alerts-default', + sizeInBytes: 29717961631, + }, + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + sizeInBytes: 0, + }, + { color: null, ilmPhase: null, index: null, pattern: 'auditbeat-*', sizeInBytes: 18820446 }, + { + color: euiThemeVars.euiColorSuccess, + ilmPhase: 'hot', + index: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + }, + { + color: euiThemeVars.euiColorDanger, + ilmPhase: 'unmanaged', + index: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + }, + { + color: null, + ilmPhase: null, + index: null, + pattern: 'packetbeat-*', + sizeInBytes: 1096520898, + }, + { + color: euiThemeVars.euiColorPrimary, + ilmPhase: 'hot', + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 584326147, + }, + { + color: euiThemeVars.euiColorPrimary, + ilmPhase: 'hot', + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 512194751, + }, + ]); + }); + }); + + describe('getFlattenedBuckets', () => { + test('it returns the expected flattened buckets', () => { + expect( + getFlattenedBuckets({ + ilmPhases, + patternRollups, + }) + ).toEqual([ + { + ilmPhase: 'hot', + incompatible: 0, + indexName: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + sizeInBytes: 0, + }, + { + ilmPhase: 'hot', + incompatible: 0, + indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', + pattern: 'auditbeat-*', + sizeInBytes: 18791790, + }, + { + ilmPhase: 'unmanaged', + incompatible: 1, + indexName: 'auditbeat-custom-empty-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 247, + }, + { + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + sizeInBytes: 28409, + }, + { + ilmPhase: 'hot', + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 512194751, + }, + { + ilmPhase: 'hot', + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + sizeInBytes: 584326147, + }, + ]); + }); + }); + + describe('getFillColor', () => { + test('it returns success when `incompatible` is zero', () => { + const incompatible = 0; + + expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorSuccess); + }); + + test('it returns danger when `incompatible` is greater than 0', () => { + const incompatible = 1; + + expect(getFillColor(incompatible)).toEqual(euiThemeVars.euiColorDanger); + }); + + test('it returns the default color when `incompatible` is undefined', () => { + const incompatible = undefined; + + expect(getFillColor(incompatible)).toEqual(DEFAULT_INDEX_COLOR); + }); + }); + + describe('getPathToFlattenedBucketMap', () => { + test('it returns the expected map', () => { + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + patternRollups, + }); + + expect(getPathToFlattenedBucketMap(flattenedBuckets)).toEqual({ + '.alerts-security.alerts-default.internal.alerts-security.alerts-default-000001': { + pattern: '.alerts-security.alerts-default', + indexName: '.internal.alerts-security.alerts-default-000001', + ilmPhase: 'hot', + incompatible: 0, + sizeInBytes: 0, + }, + 'auditbeat-*.ds-auditbeat-8.6.1-2023.02.07-000001': { + pattern: 'auditbeat-*', + indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001', + ilmPhase: 'hot', + incompatible: 0, + sizeInBytes: 18791790, + }, + 'auditbeat-*auditbeat-custom-empty-index-1': { + pattern: 'auditbeat-*', + indexName: 'auditbeat-custom-empty-index-1', + ilmPhase: 'unmanaged', + incompatible: 1, + sizeInBytes: 247, + }, + 'auditbeat-*auditbeat-custom-index-1': { + pattern: 'auditbeat-*', + indexName: 'auditbeat-custom-index-1', + ilmPhase: 'unmanaged', + incompatible: 3, + sizeInBytes: 28409, + }, + 'packetbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001': { + pattern: 'packetbeat-*', + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + ilmPhase: 'hot', + sizeInBytes: 512194751, + }, + 'packetbeat-*.ds-packetbeat-8.5.3-2023.02.04-000001': { + pattern: 'packetbeat-*', + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + ilmPhase: 'hot', + sizeInBytes: 584326147, + }, + }); + }); + }); + + describe('getGroupFromPath', () => { + it('returns the expected group from the path', () => { + expect( + getGroupFromPath([ + { + index: 0, + value: '__null_small_multiples_key__', + }, + { + index: 0, + value: '__root_key__', + }, + { + index: 0, + value: 'auditbeat-*', + }, + { + index: 1, + value: 'auditbeat-custom-empty-index-1', + }, + ]) + ).toEqual('auditbeat-*'); + }); + + it('returns undefined when path is an empty array', () => { + expect(getGroupFromPath([])).toBeUndefined(); + }); + + it('returns undefined when path is an array with only one value', () => { + expect( + getGroupFromPath([{ index: 0, value: '__null_small_multiples_key__' }]) + ).toBeUndefined(); + }); + }); + + describe('getLayersMultiDimensional', () => { + const layer0FillColor = 'transparent'; + const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + patternRollups, + }); + const pathToFlattenedBucketMap = getPathToFlattenedBucketMap(flattenedBuckets); + + it('returns the expected number of layers', () => { + expect( + getLayersMultiDimensional({ formatBytes, layer0FillColor, pathToFlattenedBucketMap }).length + ).toEqual(2); + }); + + it('returns the expected fillLabel valueFormatter function', () => { + getLayersMultiDimensional({ formatBytes, layer0FillColor, pathToFlattenedBucketMap }).forEach( + (x) => expect(x.fillLabel.valueFormatter(123)).toEqual('123B') + ); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts new file mode 100644 index 0000000000000..09ed53402a89f --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/helpers.ts @@ -0,0 +1,223 @@ +/* + * 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 type { Datum, Key, ArrayNode } from '@elastic/charts'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { orderBy } from 'lodash/fp'; + +import { getSizeInBytes } from '../../../../helpers'; +import { getIlmPhase } from '../../../pattern/helpers'; +import { PatternRollup } from '../../../../types'; + +export interface LegendItem { + color: string | null; + ilmPhase: string | null; + index: string | null; + pattern: string; + sizeInBytes: number; +} + +export interface FlattenedBucket { + ilmPhase: string | undefined; + incompatible: number | undefined; + indexName: string | undefined; + pattern: string; + sizeInBytes: number; +} + +export const getPatternSizeInBytes = ({ + pattern, + patternRollups, +}: { + pattern: string; + patternRollups: Record; +}): number => { + if (patternRollups[pattern] != null) { + return patternRollups[pattern].sizeInBytes ?? 0; + } else { + return 0; + } +}; + +export const getPatternLegendItem = ({ + pattern, + patternRollups, +}: { + pattern: string; + patternRollups: Record; +}): LegendItem => ({ + color: null, + ilmPhase: null, + index: null, + pattern, + sizeInBytes: getPatternSizeInBytes({ pattern, patternRollups }), +}); + +export const getLegendItemsForPattern = ({ + pattern, + flattenedBuckets, +}: { + pattern: string; + flattenedBuckets: FlattenedBucket[]; +}): LegendItem[] => + orderBy( + ['sizeInBytes'], + ['desc'], + flattenedBuckets + .filter((x) => x.pattern === pattern) + .map((flattenedBucket) => ({ + color: getFillColor(flattenedBucket.incompatible), + ilmPhase: flattenedBucket.ilmPhase ?? null, + index: flattenedBucket.indexName ?? null, + pattern: flattenedBucket.pattern, + sizeInBytes: flattenedBucket.sizeInBytes ?? 0, + })) + ); + +export const getLegendItems = ({ + patterns, + flattenedBuckets, + patternRollups, +}: { + patterns: string[]; + flattenedBuckets: FlattenedBucket[]; + patternRollups: Record; +}): LegendItem[] => + patterns.reduce( + (acc, pattern) => [ + ...acc, + getPatternLegendItem({ pattern, patternRollups }), + ...getLegendItemsForPattern({ pattern, flattenedBuckets }), + ], + [] + ); + +export const getFlattenedBuckets = ({ + ilmPhases, + patternRollups, +}: { + ilmPhases: string[]; + patternRollups: Record; +}): FlattenedBucket[] => + Object.values(patternRollups).reduce((acc, patternRollup) => { + // enables fast lookup of valid phase names: + const ilmPhasesMap = ilmPhases.reduce>( + (phasesMap, phase) => ({ ...phasesMap, [phase]: 0 }), + {} + ); + const { ilmExplain, pattern, results, stats } = patternRollup; + + if (ilmExplain != null && stats != null) { + return [ + ...acc, + ...Object.entries(stats).reduce( + (validStats, [indexName, indexStats]) => { + const ilmPhase = getIlmPhase(ilmExplain[indexName]); + const isSelectedPhase = ilmPhase != null && ilmPhasesMap[ilmPhase] != null; + + if (isSelectedPhase) { + const incompatible = + results != null && results[indexName] != null + ? results[indexName].incompatible + : undefined; + const sizeInBytes = getSizeInBytes({ indexName, stats }); + + return [ + ...validStats, + { + ilmPhase, + incompatible, + indexName, + pattern, + sizeInBytes, + }, + ]; + } else { + return validStats; + } + }, + [] + ), + ]; + } + + return acc; + }, []); + +const groupByRollup = (d: Datum) => d.pattern; // the treemap is grouped by this field + +export const DEFAULT_INDEX_COLOR = euiThemeVars.euiColorPrimary; + +export const getFillColor = (incompatible: number | undefined): string => { + if (incompatible === 0) { + return euiThemeVars.euiColorSuccess; + } else if (incompatible != null && incompatible > 0) { + return euiThemeVars.euiColorDanger; + } else { + return DEFAULT_INDEX_COLOR; + } +}; + +export const getPathToFlattenedBucketMap = ( + flattenedBuckets: FlattenedBucket[] +): Record => + flattenedBuckets.reduce>( + (acc, { pattern, indexName, ...remaining }) => ({ + ...acc, + [`${pattern}${indexName}`]: { pattern, indexName, ...remaining }, + }), + {} + ); + +/** + * Extracts the first group name from the data representing the second group + */ +export const getGroupFromPath = (path: ArrayNode['path']): string | undefined => { + const OFFSET_FROM_END = 2; // The offset from the end of the path array containing the group + const groupIndex = path.length - OFFSET_FROM_END; + return groupIndex > 0 ? path[groupIndex].value : undefined; +}; + +export const getLayersMultiDimensional = ({ + formatBytes, + layer0FillColor, + pathToFlattenedBucketMap, +}: { + formatBytes: (value: number | undefined) => string; + layer0FillColor: string; + pathToFlattenedBucketMap: Record; +}) => { + const valueFormatter = (d: number) => formatBytes(d); + + return [ + { + fillLabel: { + valueFormatter, + }, + groupByRollup, + nodeLabel: (ilmPhase: Datum) => ilmPhase, + shape: { + fillColor: layer0FillColor, + }, + }, + { + fillLabel: { + valueFormatter, + }, + groupByRollup: (d: Datum) => d.indexName, + nodeLabel: (indexName: Datum) => indexName, + shape: { + fillColor: (indexName: Key, sortIndex: number, node: Pick) => { + const pattern = getGroupFromPath(node.path) ?? ''; + const flattenedBucket = pathToFlattenedBucketMap[`${pattern}${indexName}`]; + + return getFillColor(flattenedBucket?.incompatible); + }, + }, + }, + ]; +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx new file mode 100644 index 0000000000000..366fb487309c3 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../../../helpers'; +import { alertIndexWithAllResults } from '../../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { PatternRollup } from '../../../../types'; +import { Props, StorageDetails } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const onIndexSelected = jest.fn(); + +const defaultProps: Props = { + formatBytes, + ilmPhases, + onIndexSelected, + patternRollups, + patterns, + theme: DARK_THEME, +}; + +describe('StorageDetails', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the treemap', () => { + expect(screen.getByTestId('storageTreemap').querySelector('.echChart')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx new file mode 100644 index 0000000000000..26340b31286fa --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/storage_details/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 type { Theme } from '@elastic/charts'; +import React, { useMemo } from 'react'; + +import { getFlattenedBuckets } from './helpers'; +import { StorageTreemap } from '../../../storage_treemap'; +import { DEFAULT_MAX_CHART_HEIGHT, StorageTreemapContainer } from '../../../tabs/styles'; +import { PatternRollup, SelectedIndex } from '../../../../types'; + +export interface Props { + formatBytes: (value: number | undefined) => string; + ilmPhases: string[]; + onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; + patternRollups: Record; + patterns: string[]; + theme: Theme; +} + +const StorageDetailsComponent: React.FC = ({ + formatBytes, + ilmPhases, + onIndexSelected, + patternRollups, + patterns, + theme, +}) => { + const flattenedBuckets = useMemo( + () => + getFlattenedBuckets({ + ilmPhases, + patternRollups, + }), + [ilmPhases, patternRollups] + ); + + return ( + + + + ); +}; + +StorageDetailsComponent.displayName = 'StorageDetailsComponent'; +export const StorageDetails = React.memo(StorageDetailsComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/translations.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/translations.ts similarity index 51% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/translations.ts rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/translations.ts index b5f77c455c1a2..6b8ffed70f8c9 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/translations.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/data_quality_details/translations.ts @@ -7,9 +7,16 @@ import { i18n } from '@kbn/i18n'; -export const TAKE_ACTION = i18n.translate( - 'ecsDataQualityDashboard.takeActionMenu.takeActionButton', +export const INDICES_TAB_TITLE = i18n.translate( + 'ecsDataQualityDashboard.body.tabs.indicesTabTitle', { - defaultMessage: 'Take action', + defaultMessage: 'Indices', + } +); + +export const STORAGE_TAB_TITLE = i18n.translate( + 'ecsDataQualityDashboard.body.tabs.storageTabTitle', + { + defaultMessage: 'Storage', } ); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx new file mode 100644 index 0000000000000..0f27f307f7913 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../helpers'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { Body } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; + +describe('IndexInvalidValues', () => { + test('it renders the data quality summary', () => { + render( + + + + ); + + expect(screen.getByTestId('dataQualitySummary')).toBeInTheDocument(); + }); + + describe('patterns', () => { + const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'logs-*', 'packetbeat-*']; + + patterns.forEach((pattern) => { + test(`it renders the '${pattern}' pattern`, () => { + render( + + + + ); + + expect(screen.getByTestId(`${pattern}PatternPanel`)).toBeInTheDocument(); + }); + }); + + test('it renders the expected number of spacers', async () => { + render( + + + + ); + + const items = await screen.findAllByTestId('bodyPatternSpacer'); + expect(items).toHaveLength(patterns.length - 1); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.tsx index 87aed178043cc..69de3b8c110e5 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/body/index.tsx @@ -17,14 +17,15 @@ import type { import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { DataQualityDetails } from './data_quality_details'; import { DataQualitySummary } from '../data_quality_summary'; -import { Pattern } from '../pattern'; import { useResultsRollup } from '../../use_results_rollup'; interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; getGroupByFieldsOnClick: ( elements: Array< | FlameElementEvent @@ -55,7 +56,8 @@ interface Props { const BodyComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, getGroupByFieldsOnClick, ilmPhases, lastChecked, @@ -72,6 +74,7 @@ const BodyComponent: React.FC = ({ totalIncompatible, totalIndices, totalIndicesChecked, + totalSizeInBytes, updatePatternIndexNames, updatePatternRollup, } = useResultsRollup({ ilmPhases, patterns }); @@ -82,7 +85,8 @@ const BodyComponent: React.FC = ({ = ({ totalIncompatible={totalIncompatible} totalIndices={totalIndices} totalIndicesChecked={totalIndicesChecked} + totalSizeInBytes={totalSizeInBytes} onCheckCompleted={onCheckCompleted} />
- {patterns.map((pattern, i) => ( - - - {i !== patterns.length - 1 ? : null} - - ))} + + +
); }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx new file mode 100644 index 0000000000000..5085db2a93e51 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.test.tsx @@ -0,0 +1,212 @@ +/* + * 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 { act, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { IndexToCheck } from '../../../types'; +import { CheckStatus, EMPTY_LAST_CHECKED_DATE } from '.'; + +const indexToCheck: IndexToCheck = { + pattern: 'auditbeat-*', + indexName: '.ds-auditbeat-8.6.1-2023.02.13-000001', +}; +const checkAllIndiciesChecked = 2; +const checkAllTotalIndiciesToCheck = 3; + +describe('CheckStatus', () => { + describe('when `indexToCheck` is not null', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it renders progress with the expected max value', () => { + expect(screen.getByTestId('progress')).toHaveAttribute( + 'max', + String(checkAllTotalIndiciesToCheck) + ); + }); + + test('it renders progress with the expected current value', () => { + expect(screen.getByTestId('progress')).toHaveAttribute( + 'value', + String(checkAllIndiciesChecked) + ); + }); + + test('it renders the expected "checking " message', () => { + expect(screen.getByTestId('checking')).toHaveTextContent( + `Checking ${indexToCheck.indexName}` + ); + }); + + test('it does NOT render the last checked message', () => { + expect(screen.queryByTestId('lastChecked')).not.toBeInTheDocument(); + }); + }); + + describe('when `indexToCheck` is null', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it does NOT render the progress bar', () => { + expect(screen.queryByTestId('progress')).not.toBeInTheDocument(); + }); + + test('it does NOT render the "checking " message', () => { + expect(screen.queryByTestId('checking')).not.toBeInTheDocument(); + }); + + test('it renders the expected last checked message', () => { + expect(screen.getByTestId('lastChecked')).toHaveTextContent(EMPTY_LAST_CHECKED_DATE); + }); + }); + + test('it renders the errors popover when errors have occurred', () => { + const errorSummary = [ + { + pattern: '.alerts-security.alerts-default', + indexName: null, + error: 'Error loading stats: Error: Forbidden', + }, + ]; + + render( + + + + ); + + expect(screen.getByTestId('errorsPopover')).toBeInTheDocument(); + }); + + test('it does NOT render the errors popover when errors have NOT occurred', () => { + render( + + + + ); + + expect(screen.queryByTestId('errorsPopover')).not.toBeInTheDocument(); + }); + + test('it invokes the `setLastChecked` callback when indexToCheck is not null', () => { + jest.useFakeTimers(); + const date = '2023-03-28T22:27:28.159Z'; + jest.setSystemTime(new Date(date)); + + const setLastChecked = jest.fn(); + + render( + + + + ); + + expect(setLastChecked).toBeCalledWith(date); + jest.useRealTimers(); + }); + + test('it updates the formatted date', async () => { + jest.useFakeTimers(); + const date = '2023-03-28T23:27:28.159Z'; + jest.setSystemTime(new Date(date)); + + const { rerender } = render( + + + + ); + + // re-render with an updated `lastChecked` + const lastChecked = '2023-03-28T22:27:28.159Z'; + + act(() => { + jest.advanceTimersByTime(1000 * 61); + }); + + rerender( + + + + ); + + act(() => { + // once again, advance time + jest.advanceTimersByTime(1000 * 61); + }); + + expect(await screen.getByTestId('lastChecked')).toHaveTextContent('Last checked: an hour ago'); + jest.useRealTimers(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx index 3ff69274ba06e..9245b0adee84c 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/check_status/index.tsx @@ -60,30 +60,31 @@ const CheckStatusComponent: React.FC = ({ }, [lastChecked]); return ( - + {indexToCheck != null && ( <> - + + {i18n.CHECKING(indexToCheck.indexName)} + - - {i18n.CHECKING(indexToCheck.indexName)} - + )} {indexToCheck == null && ( - + {i18n.LAST_CHECKED} {': '} {formattedDate} diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx new file mode 100644 index 0000000000000..064ec92a1ca81 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 userEvent from '@testing-library/user-event'; +import { act, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { ErrorsPopover } from '.'; + +const mockCopyToClipboard = jest.fn((value) => true); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: (value: string) => mockCopyToClipboard(value), + }; +}); + +const errorSummary = [ + { + pattern: '.alerts-security.alerts-default', + indexName: null, + error: 'Error loading stats: Error: Forbidden', + }, +]; + +describe('ErrorsPopover', () => { + beforeEach(() => { + document.execCommand = jest.fn(); + }); + + test('it disables the view errors button when `errorSummary` is empty', () => { + render( + + + + ); + + expect(screen.getByTestId('viewErrors')).toBeDisabled(); + }); + + test('it enables the view errors button when `errorSummary` is NOT empty', () => { + render( + + + + ); + + expect(screen.getByTestId('viewErrors')).not.toBeDisabled(); + }); + + describe('popover content', () => { + const addSuccessToast = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + render( + + + + ); + + const viewErrorsButton = screen.getByTestId('viewErrors'); + + act(() => { + userEvent.click(viewErrorsButton); + }); + }); + + test('it renders the expected callout content', () => { + expect(screen.getByTestId('callout')).toHaveTextContent( + "ErrorsSome indices were not checked for Data QualityErrors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for accessThe following privileges are required to check an index:monitor or manageview_index_metadatareadCopy to clipboard" + ); + }); + + test('it invokes `addSuccessToast` when the copy button is clicked', () => { + const copyToClipboardButton = screen.getByTestId('copyToClipboard'); + act(() => { + userEvent.click(copyToClipboardButton, undefined, { skipPointerEventsCheck: true }); + }); + + expect(addSuccessToast).toBeCalledWith({ title: 'Copied errors to the clipboard' }); + }); + + test('it renders the expected error summary text in the errors viewer', () => { + expect(screen.getByTestId('errorsViewer').textContent?.includes(errorSummary[0].error)).toBe( + true + ); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.tsx index b9e0fc61ec545..8f80e3fa3cab5 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_popover/index.tsx @@ -60,6 +60,7 @@ const ErrorsPopoverComponent: React.FC = ({ addSuccessToast, errorSummary () => ( = ({ addSuccessToast, errorSummary - +

{i18n.ERRORS_CALLOUT_SUMMARY}

{i18n.ERRORS_MAY_OCCUR}

@@ -96,7 +98,13 @@ const ErrorsPopoverComponent: React.FC = ({ addSuccessToast, errorSummary - + {i18n.COPY_TO_CLIPBOARD}
diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx new file mode 100644 index 0000000000000..1954e92ae5fc7 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.test.tsx @@ -0,0 +1,122 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { omit } from 'lodash/fp'; +import React from 'react'; + +import { getErrorsViewerTableColumns } from './helpers'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { ErrorSummary } from '../../../types'; + +const errorSummary: ErrorSummary[] = [ + { + pattern: '.alerts-security.alerts-default', + indexName: null, + error: 'Error loading stats: Error: Forbidden', + }, + { + error: + 'Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden', + indexName: 'auditbeat-7.2.1-2023.02.13-000001', + pattern: 'auditbeat-*', + }, +]; + +const noIndexName: ErrorSummary = errorSummary[0]; // <-- indexName: null +const hasIndexName: ErrorSummary = errorSummary[1]; + +describe('helpers', () => { + describe('getCommonTableColumns', () => { + test('it returns the expected column configuration', () => { + const columns = getErrorsViewerTableColumns().map((x) => omit('render', x)); + + expect(columns).toEqual([ + { + field: 'pattern', + name: 'Pattern', + sortable: true, + truncateText: false, + width: '25%', + }, + { + field: 'indexName', + name: 'Index', + sortable: false, + truncateText: false, + width: '25%', + }, + { + field: 'error', + name: 'Error', + sortable: false, + truncateText: false, + width: '50%', + }, + ]); + }); + + describe('indexName column render()', () => { + describe('when the `ErrorSummary` has an `indexName`', () => { + beforeEach(() => { + const columns = getErrorsViewerTableColumns(); + const indexNameRender = columns[1].render; + + render( + + {indexNameRender != null && indexNameRender(hasIndexName.indexName, hasIndexName)} + + ); + }); + + test('it renders the expected `indexName`', () => { + expect(screen.getByTestId('indexName')).toHaveTextContent(String(hasIndexName.indexName)); + }); + + test('it does NOT render the placeholder', () => { + expect(screen.queryByTestId('emptyPlaceholder')).not.toBeInTheDocument(); + }); + }); + + describe('when the `ErrorSummary` does NOT have an `indexName`', () => { + beforeEach(() => { + const columns = getErrorsViewerTableColumns(); + const indexNameRender = columns[1].render; + + render( + + {indexNameRender != null && indexNameRender(noIndexName.indexName, noIndexName)} + + ); + }); + + test('it does NOT render `indexName`', () => { + expect(screen.queryByTestId('indexName')).not.toBeInTheDocument(); + }); + + test('it renders the placeholder', () => { + expect(screen.getByTestId('emptyPlaceholder')).toBeInTheDocument(); + }); + }); + }); + + describe('indexName error render()', () => { + test('it renders the expected `error`', () => { + const columns = getErrorsViewerTableColumns(); + const indexNameRender = columns[2].render; + + render( + + {indexNameRender != null && indexNameRender(hasIndexName.error, hasIndexName)} + + ); + + expect(screen.getByTestId('error')).toHaveTextContent(hasIndexName.error); + }); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.tsx index caac710cf8d13..35a4a74cca875 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/helpers.tsx @@ -28,7 +28,12 @@ export const getErrorsViewerTableColumns = (): Array (indexName != null && indexName !== '' ? indexName : EMPTY_PLACEHOLDER), + render: (indexName: string | null) => + indexName != null && indexName !== '' ? ( + {indexName} + ) : ( + {EMPTY_PLACEHOLDER} + ), sortable: false, truncateText: false, width: '25%', @@ -36,7 +41,7 @@ export const getErrorsViewerTableColumns = (): Array {errorText}, + render: (errorText) => {errorText}, sortable: false, truncateText: false, width: '50%', diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx new file mode 100644 index 0000000000000..a1b6346eb2b8d --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { ERROR, INDEX, PATTERN } from './translations'; +import { ErrorSummary } from '../../../types'; +import { ErrorsViewer } from '.'; + +interface ExpectedColumns { + id: string; + expected: string; +} + +const errorSummary: ErrorSummary[] = [ + { + pattern: '.alerts-security.alerts-default', + indexName: null, + error: 'Error loading stats: Error: Forbidden', + }, + { + error: + 'Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden', + indexName: 'auditbeat-7.2.1-2023.02.13-000001', + pattern: 'auditbeat-*', + }, +]; + +describe('ErrorsViewer', () => { + const expectedColumns: ExpectedColumns[] = [ + { + id: 'pattern', + expected: PATTERN, + }, + { + id: 'indexName', + expected: INDEX, + }, + { + id: 'error', + expected: ERROR, + }, + ]; + + expectedColumns.forEach(({ id, expected }, i) => { + test(`it renders the expected '${id}' column header`, () => { + render( + + + + ); + + expect(screen.getByTestId(`tableHeaderCell_${id}_${i}`)).toHaveTextContent(expected); + }); + }); + + test(`it renders the expected the errors`, () => { + render( + + + + ); + + expect( + screen + .getAllByTestId('error') + .map((x) => x.textContent ?? '') + .reduce((acc, x) => acc.concat(x), '') + ).toEqual(`${errorSummary[0].error}${errorSummary[1].error}`); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.tsx index a40094c96b399..2336abe79c651 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/errors_viewer/index.tsx @@ -29,7 +29,7 @@ const ErrorsViewerComponent: React.FC = ({ errorSummary }) => { const columns = useMemo(() => getErrorsViewerTableColumns(), []); return ( - + + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const patternIndexNames: Record = { + 'auditbeat-*': [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-custom-index-1', + ], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], + 'packetbeat-*': [ + '.ds-packetbeat-8.5.3-2023.02.04-000001', + '.ds-packetbeat-8.6.1-2023.02.04-000001', + ], +}; + +const lastChecked = '2023-03-28T23:27:28.159Z'; + +const totalDocsCount = getTotalDocsCount(patternRollups); +const totalIncompatible = getTotalIncompatible(patternRollups); +const totalIndices = getTotalIndices(patternRollups); +const totalIndicesChecked = getTotalIndicesChecked(patternRollups); +const totalSizeInBytes = getTotalSizeInBytes(patternRollups); + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: jest.fn(), + formatBytes, + formatNumber, + ilmPhases, + lastChecked, + openCreateCaseFlyout: jest.fn(), + patternIndexNames, + patternRollups, + patterns, + setLastChecked: jest.fn(), + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + totalSizeInBytes, + onCheckCompleted: jest.fn(), +}; + +describe('DataQualitySummary', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the summary actions', () => { + expect(screen.getByTestId('summaryActions')).toBeInTheDocument(); + }); + + test('it renders the stats rollup', () => { + expect(screen.getByTestId('statsRollup')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx index c6874d861ddb8..d3f9ad9d23303 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/index.tsx @@ -5,15 +5,14 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { CheckStatus } from './check_status'; +import { getErrorSummaries } from '../../helpers'; import { StatsRollup } from '../pattern/pattern_summary/stats_rollup'; import { SummaryActions } from './summary_actions'; -import type { IndexToCheck, OnCheckCompleted, PatternRollup } from '../../types'; -import { getErrorSummaries } from '../../helpers'; +import type { OnCheckCompleted, PatternRollup } from '../../types'; const MAX_SUMMARY_ACTIONS_CONTAINER_WIDTH = 400; const MIN_SUMMARY_ACTIONS_CONTAINER_WIDTH = 235; @@ -24,10 +23,11 @@ const SummaryActionsContainerFlexItem = styled(EuiFlexItem)` padding-right: ${({ theme }) => theme.eui.euiSizeXL}; `; -interface Props { +export interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmPhases: string[]; lastChecked: string; openCreateCaseFlyout: ({ @@ -45,13 +45,15 @@ interface Props { totalIncompatible: number | undefined; totalIndices: number | undefined; totalIndicesChecked: number | undefined; + totalSizeInBytes: number | undefined; onCheckCompleted: OnCheckCompleted; } const DataQualitySummaryComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, ilmPhases, lastChecked, openCreateCaseFlyout, @@ -63,64 +65,46 @@ const DataQualitySummaryComponent: React.FC = ({ totalIncompatible, totalIndices, totalIndicesChecked, + totalSizeInBytes, onCheckCompleted, }) => { - const [indexToCheck, setIndexToCheck] = useState(null); - - const [checkAllIndiciesChecked, setCheckAllIndiciesChecked] = useState(0); - const [checkAllTotalIndiciesToCheck, setCheckAllTotalIndiciesToCheck] = useState(0); - - const incrementCheckAllIndiciesChecked = useCallback(() => { - setCheckAllIndiciesChecked((current) => current + 1); - }, []); - const errorSummary = useMemo(() => getErrorSummaries(patternRollups), [patternRollups]); return ( - + - - - - diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx new file mode 100644 index 0000000000000..02b04225a544e --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { Props, Actions } from '.'; + +const mockCopyToClipboard = jest.fn((value) => true); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: (value: string) => mockCopyToClipboard(value), + }; +}); + +const ilmPhases = ['hot', 'warm', 'unmanaged']; + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: () => true, + getMarkdownComments: () => [], + ilmPhases, + openCreateCaseFlyout: jest.fn(), +}; + +describe('Actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('when the action buttons are clicked', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it invokes openCreateCaseFlyout when the add to new case button is clicked', () => { + const button = screen.getByTestId('addToNewCase'); + + userEvent.click(button); + + expect(defaultProps.openCreateCaseFlyout).toBeCalled(); + }); + + test('it invokes addSuccessToast when the copy to clipboard button is clicked', () => { + const button = screen.getByTestId('copyToClipboard'); + + userEvent.click(button); + + expect(defaultProps.addSuccessToast).toBeCalledWith({ + title: 'Copied results to the clipboard', + }); + }); + }); + + test('it disables the add to new case button when the user cannot create cases', () => { + const canUserCreateAndReadCases = () => false; + + render( + + + + ); + + const button = screen.getByTestId('addToNewCase'); + + expect(button).toBeDisabled(); + }); + + test('it disables the add to new case button when `ilmPhases` is empty', () => { + render( + + + + ); + + const button = screen.getByTestId('addToNewCase'); + + expect(button).toBeDisabled(); + }); + + test('it disables the copy to clipboard button when `ilmPhases` is empty', () => { + render( + + + + ); + + const button = screen.getByTestId('copyToClipboard'); + + expect(button).toBeDisabled(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx new file mode 100644 index 0000000000000..549f420c3fa9d --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/actions/index.tsx @@ -0,0 +1,98 @@ +/* + * 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 { copyToClipboard, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { + ADD_TO_NEW_CASE, + COPIED_RESULTS_TOAST_TITLE, + COPY_TO_CLIPBOARD, +} from '../../../../translations'; +import { useAddToNewCase } from '../../../../use_add_to_new_case'; + +export interface Props { + addSuccessToast: (toast: { title: string }) => void; + canUserCreateAndReadCases: () => boolean; + getMarkdownComments: () => string[]; + ilmPhases: string[]; + openCreateCaseFlyout: ({ + comments, + headerContent, + }: { + comments: string[]; + headerContent?: React.ReactNode; + }) => void; +} + +const ActionsComponent: React.FC = ({ + addSuccessToast, + canUserCreateAndReadCases, + getMarkdownComments, + ilmPhases, + openCreateCaseFlyout, +}) => { + const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({ + canUserCreateAndReadCases, + openCreateCaseFlyout, + }); + + const onClickAddToCase = useCallback( + () => onAddToNewCase([getMarkdownComments().join('\n')]), + [getMarkdownComments, onAddToNewCase] + ); + + const onCopy = useCallback(() => { + const markdown = getMarkdownComments().join('\n'); + copyToClipboard(markdown); + + addSuccessToast({ + title: COPIED_RESULTS_TOAST_TITLE, + }); + }, [addSuccessToast, getMarkdownComments]); + + const addToNewCaseContextMenuOnClick = useCallback(() => { + onClickAddToCase(); + }, [onClickAddToCase]); + + const disableAll = ilmPhases.length === 0; + + return ( + + + + {ADD_TO_NEW_CASE} + + + + + + {COPY_TO_CLIPBOARD} + + + + ); +}; + +ActionsComponent.displayName = 'ActionsComponent'; + +export const Actions = React.memo(ActionsComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts new file mode 100644 index 0000000000000..fd457193a9c6f --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.test.ts @@ -0,0 +1,338 @@ +/* + * 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 { EcsFlat, EcsVersion } from '@kbn/ecs'; + +import { checkIndex, EMPTY_PARTITIONED_FIELD_METADATA } from './check_index'; +import { EMPTY_STAT } from '../../../../helpers'; +import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response'; +import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values'; +import { EcsMetadata, UnallowedValueRequestItem } from '../../../../types'; + +const ecsMetadata = EcsFlat as unknown as Record; + +let mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + new Promise((resolve) => { + resolve(mockMappingsResponse); // happy path + }) +); + +jest.mock('../../../../use_mappings/helpers', () => ({ + fetchMappings: ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + mockFetchMappings({ + abortController, + patternOrIndexName, + }), +})); + +const mockFetchUnallowedValues = jest.fn( + ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) +); + +jest.mock('../../../../use_unallowed_values/helpers', () => { + const original = jest.requireActual('../../../../use_unallowed_values/helpers'); + + return { + ...original, + fetchUnallowedValues: ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => + mockFetchUnallowedValues({ + abortController, + indexName, + requestItems, + }), + }; +}); + +describe('checkIndex', () => { + const defaultBytesFormat = '0,0.[0]b'; + const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + + const defaultNumberFormat = '0,0.[000]'; + const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + + const indexName = 'auditbeat-custom-index-1'; + const pattern = 'auditbeat-*'; + + describe('happy path', () => { + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await checkIndex({ + abortController: new AbortController(), + ecsMetadata, + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + }); + + test('it invokes onCheckCompleted with a null `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with the non-default `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).not.toEqual( + EMPTY_PARTITIONED_FIELD_METADATA + ); + }); + + test('it invokes onCheckCompleted with the expected`pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + + test('it invokes onCheckCompleted with the expected `version`', () => { + expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); + }); + }); + + describe('happy path, when the signal is aborted', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it does NOT invoke onCheckCompleted', async () => { + const onCheckCompleted = jest.fn(); + + const abortController = new AbortController(); + abortController.abort(); + + await checkIndex({ + abortController, + ecsMetadata, + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + + expect(onCheckCompleted).not.toBeCalled(); + }); + }); + + describe('when `ecsMetadata` is null', () => { + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + await checkIndex({ + abortController: new AbortController(), + ecsMetadata: null, // <-- + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + }); + + test('it invokes onCheckCompleted with a null `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with the default `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toEqual( + EMPTY_PARTITIONED_FIELD_METADATA + ); + }); + + test('it invokes onCheckCompleted with the expected `pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + + test('it invokes onCheckCompleted with the expected `version`', () => { + expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); + }); + }); + + describe('when an error occurs', () => { + const onCheckCompleted = jest.fn(); + const error = 'simulated fetch mappings error'; + + beforeEach(async () => { + jest.clearAllMocks(); + + mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => new Promise((_, reject) => reject(new Error(error))) + ); + + await checkIndex({ + abortController: new AbortController(), + ecsMetadata, + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + }); + + test('it invokes onCheckCompleted with the expected `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toEqual(`Error: ${error}`); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + + test('it invokes onCheckCompleted with the expected `version`', () => { + expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); + }); + }); + + describe('when an error occurs, but the error does not have a toString', () => { + const onCheckCompleted = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + + mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + // eslint-disable-next-line prefer-promise-reject-errors + }) => new Promise((_, reject) => reject(undefined)) + ); + + await checkIndex({ + abortController: new AbortController(), + ecsMetadata, + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + }); + + test('it invokes onCheckCompleted with the fallback `error`', () => { + expect(onCheckCompleted.mock.calls[0][0].error).toEqual( + `An error occurred checking index ${indexName}` + ); + }); + + test('it invokes onCheckCompleted with the expected `indexName`', () => { + expect(onCheckCompleted.mock.calls[0][0].indexName).toEqual(indexName); + }); + + test('it invokes onCheckCompleted with null `partitionedFieldMetadata`', () => { + expect(onCheckCompleted.mock.calls[0][0].partitionedFieldMetadata).toBeNull(); + }); + + test('it invokes onCheckCompleted with the expected `pattern`', () => { + expect(onCheckCompleted.mock.calls[0][0].pattern).toEqual(pattern); + }); + + test('it invokes onCheckCompleted with the expected `version`', () => { + expect(onCheckCompleted.mock.calls[0][0].version).toEqual(EcsVersion); + }); + }); + + describe('when an error occurs, and the signal is aborted', () => { + const onCheckCompleted = jest.fn(); + const abortController = new AbortController(); + abortController.abort(); + + const error = 'simulated fetch mappings error'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it does NOT invoke onCheckCompleted', async () => { + mockFetchMappings = jest.fn( + ({ + // eslint-disable-next-line @typescript-eslint/no-shadow + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => new Promise((_, reject) => reject(new Error(error))) + ); + + await checkIndex({ + abortController, + ecsMetadata, + formatBytes, + formatNumber, + indexName, + onCheckCompleted, + pattern, + version: EcsVersion, + }); + + expect(onCheckCompleted).not.toBeCalled(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts index cd1ce0940b391..c65b3f7559071 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/check_index.ts @@ -15,7 +15,7 @@ import type { EcsMetadata, OnCheckCompleted, PartitionedFieldMetadata } from '.. import { fetchMappings } from '../../../../use_mappings/helpers'; import { fetchUnallowedValues, getUnallowedValues } from '../../../../use_unallowed_values/helpers'; -const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { +export const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { all: [], custom: [], ecsCompliant: [], @@ -25,6 +25,7 @@ const EMPTY_PARTITIONED_FIELD_METADATA: PartitionedFieldMetadata = { export async function checkIndex({ abortController, ecsMetadata, + formatBytes, formatNumber, indexName, onCheckCompleted, @@ -33,6 +34,7 @@ export async function checkIndex({ }: { abortController: AbortController; ecsMetadata: Record | null; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; onCheckCompleted: OnCheckCompleted; @@ -74,6 +76,7 @@ export async function checkIndex({ if (!abortController.signal.aborted) { onCheckCompleted({ error: null, + formatBytes, formatNumber, indexName, partitionedFieldMetadata, @@ -84,10 +87,8 @@ export async function checkIndex({ } catch (error) { if (!abortController.signal.aborted) { onCheckCompleted({ - error: - error.toString() != null - ? error.toString() - : i18n.AN_ERROR_OCCURRED_CHECKING_INDEX(indexName), + error: error != null ? error.toString() : i18n.AN_ERROR_OCCURRED_CHECKING_INDEX(indexName), + formatBytes, formatNumber, indexName, partitionedFieldMetadata: null, diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts new file mode 100644 index 0000000000000..5f96cfa9953a6 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.test.ts @@ -0,0 +1,107 @@ +/* + * 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 { getAllIndicesToCheck, getIndexDocsCountFromRollup, getIndexToCheck } from './helpers'; +import { mockPacketbeatPatternRollup } from '../../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; + +const patternIndexNames: Record = { + 'packetbeat-*': [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + ], + 'auditbeat-*': [ + 'auditbeat-7.17.9-2023.02.13-000001', + 'auditbeat-custom-index-1', + '.ds-auditbeat-8.6.1-2023.02.13-000001', + ], + 'logs-*': [ + '.ds-logs-endpoint.alerts-default-2023.02.24-000001', + '.ds-logs-endpoint.events.process-default-2023.02.24-000001', + ], + 'remote:*': [], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], +}; + +describe('helpers', () => { + describe('getIndexToCheck', () => { + test('it returns the expected `IndexToCheck`', () => { + expect( + getIndexToCheck({ + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }) + ).toEqual({ + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }); + }); + }); + + describe('getAllIndicesToCheck', () => { + test('it returns the sorted collection of `IndexToCheck`', () => { + expect(getAllIndicesToCheck(patternIndexNames)).toEqual([ + { + indexName: '.internal.alerts-security.alerts-default-000001', + pattern: '.alerts-security.alerts-default', + }, + { + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + }, + { + indexName: 'auditbeat-7.17.9-2023.02.13-000001', + pattern: 'auditbeat-*', + }, + { + indexName: '.ds-auditbeat-8.6.1-2023.02.13-000001', + pattern: 'auditbeat-*', + }, + { + indexName: '.ds-logs-endpoint.events.process-default-2023.02.24-000001', + pattern: 'logs-*', + }, + { + indexName: '.ds-logs-endpoint.alerts-default-2023.02.24-000001', + pattern: 'logs-*', + }, + { + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'packetbeat-*', + }, + { + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'packetbeat-*', + }, + ]); + }); + }); + + describe('getIndexDocsCountFromRollup', () => { + test('it returns the expected count when the `patternRollup` has `stats`', () => { + expect( + getIndexDocsCountFromRollup({ + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + patternRollup: mockPacketbeatPatternRollup, + }) + ).toEqual(1628343); + }); + + test('it returns zero when the `patternRollup` `stats` is null', () => { + const patternRollup = { + ...mockPacketbeatPatternRollup, + stats: null, // <-- + }; + + expect( + getIndexDocsCountFromRollup({ + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + patternRollup, + }) + ).toEqual(0); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts index 4d07d4826f521..30f314c73ea3b 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/helpers.ts @@ -6,7 +6,7 @@ */ import type { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; -import { sortBy } from 'lodash/fp'; +import { orderBy } from 'lodash/fp'; import { getDocsCount } from '../../../../helpers'; import type { IndexToCheck, PatternRollup } from '../../../../types'; @@ -34,14 +34,14 @@ export const getAllIndicesToCheck = ( return a.localeCompare(b); }); - // return all `IndexToCheck` sorted first by pattern A-Z, and then by `docsCount` within the pattern + // return all `IndexToCheck` sorted first by pattern A-Z: return sortedPatterns.reduce((acc, pattern) => { - const indexNames = patternIndexNames[pattern] ?? []; + const indexNames = patternIndexNames[pattern]; const indicesToCheck = indexNames.map((indexName) => getIndexToCheck({ indexName, pattern }) ); - const sortedIndicesToCheck = sortBy('indexName', indicesToCheck).reverse(); + const sortedIndicesToCheck = orderBy(['indexName'], ['desc'], indicesToCheck); return [...acc, ...sortedIndicesToCheck]; }, []); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx new file mode 100644 index 0000000000000..f2aa7a2666c33 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.test.tsx @@ -0,0 +1,377 @@ +/* + * 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 numeral from '@elastic/numeral'; +import userEvent from '@testing-library/user-event'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { mockMappingsResponse } from '../../../../mock/mappings_response/mock_mappings_response'; +import { TestProviders } from '../../../../mock/test_providers/test_providers'; +import { mockUnallowedValuesResponse } from '../../../../mock/unallowed_values/mock_unallowed_values'; +import { CANCEL, CHECK_ALL } from '../../../../translations'; +import { + OnCheckCompleted, + PartitionedFieldMetadata, + UnallowedValueRequestItem, +} from '../../../../types'; +import { CheckAll } from '.'; +import { EMPTY_STAT } from '../../../../helpers'; + +const defaultBytesFormat = '0,0.[0]b'; +const mockFormatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const mockFormatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + new Promise((resolve) => { + resolve(mockMappingsResponse); // happy path + }) +); + +jest.mock('../../../../use_mappings/helpers', () => ({ + fetchMappings: ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + mockFetchMappings({ + abortController, + patternOrIndexName, + }), +})); + +const mockFetchUnallowedValues = jest.fn( + ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) +); + +jest.mock('../../../../use_unallowed_values/helpers', () => { + const original = jest.requireActual('../../../../use_unallowed_values/helpers'); + + return { + ...original, + fetchUnallowedValues: ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => + mockFetchUnallowedValues({ + abortController, + indexName, + requestItems, + }), + }; +}); + +const patternIndexNames = { + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], + 'auditbeat-*': [ + 'auditbeat-7.3.2-2023.03.27-000001', + '.ds-auditbeat-8.6.1-2023.03.29-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-7.10.2-2023.03.27-000001', + 'auditbeat-7.2.1-2023.03.27-000001', + 'auditbeat-custom-index-1', + ], + 'logs-*': [ + '.ds-logs-endpoint.events.process-default-2023.03.27-000001', + '.ds-logs-endpoint.alerts-default-2023.03.27-000001', + ], + 'packetbeat-*': [ + '.ds-packetbeat-8.6.1-2023.03.27-000001', + '.ds-packetbeat-8.5.3-2023.03.27-000001', + ], +}; + +const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; + +describe('CheckAll', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it renders the expected button text when a check is NOT running', () => { + render( + + + + ); + + expect(screen.getByTestId('checkAll')).toHaveTextContent(CHECK_ALL); + }); + + test('it renders the expected button text when a check is running', () => { + render( + + + + ); + + const button = screen.getByTestId('checkAll'); + + userEvent.click(button); // <-- START the check + + expect(screen.getByTestId('checkAll')).toHaveTextContent(CANCEL); + }); + + describe('formatNumber', () => { + test('it renders a comma-separated `value` via the `defaultNumberFormat`', async () => { + /** stores the result of invoking `CheckAll`'s `formatNumber` function */ + let formatNumberResult = ''; + + const onCheckCompleted: OnCheckCompleted = jest.fn( + ({ + formatBytes, + formatNumber, + }: { + error: string | null; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + indexName: string; + partitionedFieldMetadata: PartitionedFieldMetadata | null; + pattern: string; + version: string; + }) => { + const value = 123456789; // numeric input to `CheckAll`'s `formatNumber` function + + formatNumberResult = formatNumber(value); + } + ); + + render( + + + + ); + + const button = screen.getByTestId('checkAll'); + + userEvent.click(button); // <-- START the check + + await waitFor(() => { + expect(formatNumberResult).toEqual('123,456,789'); // a comma-separated `value`, because it's numeric + }); + }); + + test('it renders an empty stat placeholder when `value` is undefined', async () => { + /** stores the result of invoking `CheckAll`'s `formatNumber` function */ + let formatNumberResult = ''; + + const onCheckCompleted: OnCheckCompleted = jest.fn( + ({ + formatBytes, + formatNumber, + }: { + error: string | null; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; + indexName: string; + partitionedFieldMetadata: PartitionedFieldMetadata | null; + pattern: string; + version: string; + }) => { + const value = undefined; // undefined input to `CheckAll`'s `formatNumber` function + + formatNumberResult = formatNumber(value); + } + ); + + render( + + + + ); + + const button = screen.getByTestId('checkAll'); + + userEvent.click(button); // <-- START the check + + await waitFor(() => { + expect(formatNumberResult).toEqual(EMPTY_STAT); // a placeholder, because `value` is undefined + }); + }); + }); + + describe('when a running check is cancelled', () => { + const setCheckAllIndiciesChecked = jest.fn(); + const setCheckAllTotalIndiciesToCheck = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + + const button = screen.getByTestId('checkAll'); + + userEvent.click(button); // <-- START the check + + userEvent.click(button); // <-- STOP the check + }); + + test('it invokes `setCheckAllIndiciesChecked` twice: when the check was started, and when it was cancelled', () => { + expect(setCheckAllIndiciesChecked).toHaveBeenCalledTimes(2); + }); + + test('it invokes `setCheckAllTotalIndiciesToCheck` with the expected index count when the check is STARTED', () => { + expect(setCheckAllTotalIndiciesToCheck.mock.calls[0][0]).toEqual(11); + }); + + test('it invokes `setCheckAllTotalIndiciesToCheck` with the expected index count when the check is STOPPED', () => { + expect(setCheckAllTotalIndiciesToCheck.mock.calls[1][0]).toEqual(0); + }); + }); + + describe('when all checks have completed', () => { + const setIndexToCheck = jest.fn(); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + render( + + + + ); + + const button = screen.getByTestId('checkAll'); + + userEvent.click(button); // <-- start the check + + const totalIndexNames = Object.values(patternIndexNames).reduce( + (total, indices) => total + indices.length, + 0 + ); + + // simulate the wall clock advancing + for (let i = 0; i < totalIndexNames + 1; i++) { + act(() => { + jest.advanceTimersByTime(1000 * 10); + }); + + await waitFor(() => {}); + } + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('it invokes setIndexToCheck with `null` after all the checks have completed', () => { + expect(setIndexToCheck).toBeCalledWith(null); + }); + + // test all the patterns + Object.entries(patternIndexNames).forEach((pattern) => { + const [patternName, indexNames] = pattern; + + // test each index in the pattern + indexNames.forEach((indexName) => { + test(`it invokes setIndexToCheck with the expected value for the '${patternName}' pattern's index, named '${indexName}'`, () => { + expect(setIndexToCheck).toBeCalledWith({ + indexName, + pattern: patternName, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx index ab5de8d7ccdcb..ef768249aa2a4 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/check_all/index.tsx @@ -8,15 +8,18 @@ import { EcsFlat, EcsVersion } from '@kbn/ecs'; import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; import { checkIndex } from './check_index'; -import { EMPTY_STAT } from '../../../../helpers'; import { getAllIndicesToCheck } from './helpers'; import * as i18n from '../../../../translations'; import type { EcsMetadata, IndexToCheck, OnCheckCompleted } from '../../../../types'; +const CheckAllButton = styled(EuiButton)` + width: 112px; +`; + async function wait(ms: number) { const delay = () => new Promise((resolve) => @@ -29,7 +32,8 @@ async function wait(ms: number) { } interface Props { - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmPhases: string[]; incrementCheckAllIndiciesChecked: () => void; onCheckCompleted: OnCheckCompleted; @@ -43,7 +47,8 @@ interface Props { const DELAY_AFTER_EVERY_CHECK_COMPLETES = 3000; // ms const CheckAllComponent: React.FC = ({ - defaultNumberFormat, + formatBytes, + formatNumber, ilmPhases, incrementCheckAllIndiciesChecked, onCheckCompleted, @@ -55,11 +60,6 @@ const CheckAllComponent: React.FC = ({ }) => { const abortController = useRef(new AbortController()); const [isRunning, setIsRunning] = useState(false); - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); const cancelIfRunning = useCallback(() => { if (isRunning) { @@ -89,6 +89,7 @@ const CheckAllComponent: React.FC = ({ await checkIndex({ abortController: abortController.current, ecsMetadata: EcsFlat as unknown as Record, + formatBytes, formatNumber, indexName, onCheckCompleted, @@ -118,6 +119,7 @@ const CheckAllComponent: React.FC = ({ } }, [ cancelIfRunning, + formatBytes, formatNumber, incrementCheckAllIndiciesChecked, isRunning, @@ -141,14 +143,18 @@ const CheckAllComponent: React.FC = ({ }; }, [abortController]); + const disabled = ilmPhases.length === 0; + return ( - {isRunning ? i18n.CANCEL : i18n.CHECK_ALL} - + ); }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx new file mode 100644 index 0000000000000..7d139121afbc4 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.test.tsx @@ -0,0 +1,128 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EMPTY_STAT } from '../../../helpers'; +import { alertIndexWithAllResults } from '../../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { TestProviders } from '../../../mock/test_providers/test_providers'; +import { PatternRollup } from '../../../types'; +import { Props, SummaryActions } from '.'; +import { + getTotalDocsCount, + getTotalIncompatible, + getTotalIndices, + getTotalIndicesChecked, + getTotalSizeInBytes, +} from '../../../use_results_rollup/helpers'; + +const mockCopyToClipboard = jest.fn((value) => true); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + copyToClipboard: (value: string) => mockCopyToClipboard(value), + }; +}); + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const patternIndexNames: Record = { + 'auditbeat-*': [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-custom-index-1', + ], + '.alerts-security.alerts-default': ['.internal.alerts-security.alerts-default-000001'], + 'packetbeat-*': [ + '.ds-packetbeat-8.5.3-2023.02.04-000001', + '.ds-packetbeat-8.6.1-2023.02.04-000001', + ], +}; + +const lastChecked = '2023-03-28T23:27:28.159Z'; + +const totalDocsCount = getTotalDocsCount(patternRollups); +const totalIncompatible = getTotalIncompatible(patternRollups); +const totalIndices = getTotalIndices(patternRollups); +const totalIndicesChecked = getTotalIndicesChecked(patternRollups); +const totalSizeInBytes = getTotalSizeInBytes(patternRollups); + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: () => true, + errorSummary: [], + formatBytes, + formatNumber, + ilmPhases, + lastChecked, + openCreateCaseFlyout: jest.fn(), + onCheckCompleted: jest.fn(), + patternIndexNames, + patternRollups, + patterns, + setLastChecked: jest.fn(), + totalDocsCount, + totalIncompatible, + totalIndices, + totalIndicesChecked, + sizeInBytes: totalSizeInBytes, +}; + +describe('SummaryActions', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the check all button', () => { + expect(screen.getByTestId('checkAll')).toBeInTheDocument(); + }); + + test('it renders the check status indicator', () => { + expect(screen.getByTestId('checkStatus')).toBeInTheDocument(); + }); + + test('it renders the actions', () => { + expect(screen.getByTestId('actions')).toBeInTheDocument(); + }); + + test('it invokes addSuccessToast when the copy to clipboard button is clicked', () => { + const button = screen.getByTestId('copyToClipboard'); + + userEvent.click(button); + + expect(defaultProps.addSuccessToast).toBeCalledWith({ + title: 'Copied results to the clipboard', + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx index d659d81d93199..db53376746281 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/index.tsx @@ -6,15 +6,14 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import { sortBy } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { CheckAll } from './check_all'; +import { CheckStatus } from '../check_status'; import { ERROR, INDEX, PATTERN } from '../errors_viewer/translations'; import { ERRORS } from '../errors_popover/translations'; -import { EMPTY_STAT } from '../../../helpers'; import { getDataQualitySummaryMarkdownComment, getErrorsMarkdownTable, @@ -23,8 +22,8 @@ import { getSummaryTableMarkdownHeader, getSummaryTableMarkdownRow, } from '../../index_properties/markdown/helpers'; -import { getSummaryTableItems } from '../../pattern/helpers'; -import { TakeActionMenu } from './take_action_menu'; +import { defaultSort, getSummaryTableItems } from '../../pattern/helpers'; +import { Actions } from './actions'; import type { DataQualityCheckResult, ErrorSummary, @@ -32,6 +31,7 @@ import type { OnCheckCompleted, PatternRollup, } from '../../../types'; +import { getSizeInBytes } from '../../../helpers'; const SummaryActionsFlexGroup = styled(EuiFlexGroup)` gap: ${({ theme }) => theme.eui.euiSizeS}; @@ -43,10 +43,12 @@ export const getResultsSortedByDocsCount = ( results != null ? sortBy('docsCount', Object.values(results)).reverse() : []; export const getAllMarkdownCommentsFromResults = ({ + formatBytes, formatNumber, patternIndexNames, patternRollup, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; patternIndexNames: Record; patternRollup: PatternRollup; @@ -60,6 +62,8 @@ export const getAllMarkdownCommentsFromResults = ({ pattern: patternRollup.pattern, patternDocsCount: patternRollup.docsCount ?? 0, results: patternRollup.results, + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, stats: patternRollup.stats, }); @@ -69,11 +73,13 @@ export const getAllMarkdownCommentsFromResults = ({ return getSummaryTableMarkdownRow({ docsCount: item.docsCount, + formatBytes, formatNumber, ilmPhase: item.ilmPhase, indexName: item.indexName, incompatible: result?.incompatible, patternDocsCount: patternRollup.docsCount ?? 0, + sizeInBytes: getSizeInBytes({ indexName: item.indexName, stats: patternRollup.stats }), }).trim(); }); @@ -89,10 +95,12 @@ export const getAllMarkdownCommentsFromResults = ({ }; export const getAllMarkdownComments = ({ + formatBytes, formatNumber, patternIndexNames, patternRollups, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; patternIndexNames: Record; patternRollups: Record; @@ -108,10 +116,12 @@ export const getAllMarkdownComments = ({ (acc, pattern) => [ ...acc, getPatternSummaryMarkdownComment({ + formatBytes, formatNumber, patternRollup: patternRollups[pattern], }), ...getAllMarkdownCommentsFromResults({ + formatBytes, formatNumber, patternRollup: patternRollups[pattern], patternIndexNames, @@ -121,13 +131,14 @@ export const getAllMarkdownComments = ({ ); }; -interface Props { +export interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; errorSummary: ErrorSummary[]; ilmPhases: string[]; - incrementCheckAllIndiciesChecked: () => void; + lastChecked: string; onCheckCompleted: OnCheckCompleted; openCreateCaseFlyout: ({ comments, @@ -139,51 +150,54 @@ interface Props { patternIndexNames: Record; patternRollups: Record; patterns: string[]; - setCheckAllIndiciesChecked: (checkAllIndiciesChecked: number) => void; - setCheckAllTotalIndiciesToCheck: (checkAllTotalIndiciesToCheck: number) => void; - setIndexToCheck: (indexToCheck: IndexToCheck | null) => void; + setLastChecked: (lastChecked: string) => void; totalDocsCount: number | undefined; totalIncompatible: number | undefined; totalIndices: number | undefined; totalIndicesChecked: number | undefined; + sizeInBytes: number | undefined; } const SummaryActionsComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, errorSummary, ilmPhases, - incrementCheckAllIndiciesChecked, + lastChecked, onCheckCompleted, openCreateCaseFlyout, patternIndexNames, patternRollups, patterns, + setLastChecked, totalDocsCount, - setCheckAllIndiciesChecked, - setCheckAllTotalIndiciesToCheck, - setIndexToCheck, totalIncompatible, totalIndices, totalIndicesChecked, + sizeInBytes, }) => { - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); + const [indexToCheck, setIndexToCheck] = useState(null); + const [checkAllIndiciesChecked, setCheckAllIndiciesChecked] = useState(0); + const [checkAllTotalIndiciesToCheck, setCheckAllTotalIndiciesToCheck] = useState(0); + const incrementCheckAllIndiciesChecked = useCallback(() => { + setCheckAllIndiciesChecked((current) => current + 1); + }, []); const getMarkdownComments = useCallback( (): string[] => [ getDataQualitySummaryMarkdownComment({ + formatBytes, formatNumber, totalDocsCount, totalIncompatible, totalIndices, totalIndicesChecked, + sizeInBytes, }), ...getAllMarkdownComments({ + formatBytes, formatNumber, patternIndexNames, patternRollups, @@ -197,9 +211,11 @@ const SummaryActionsComponent: React.FC = ({ ], [ errorSummary, + formatBytes, formatNumber, patternIndexNames, patternRollups, + sizeInBytes, totalDocsCount, totalIncompatible, totalIndices, @@ -208,30 +224,46 @@ const SummaryActionsComponent: React.FC = ({ ); return ( - - - - - - - - - + <> + + + + + + + + + + + + + + ); }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/index.tsx deleted file mode 100644 index 4683a96a76987..0000000000000 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/data_quality_summary/summary_actions/take_action_menu/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 { - copyToClipboard, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiPopover, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; - -import { - ADD_TO_NEW_CASE, - COPIED_RESULTS_TOAST_TITLE, - COPY_TO_CLIPBOARD, -} from '../../../../translations'; -import * as i18n from './translations'; -import { useAddToNewCase } from '../../../../use_add_to_new_case'; - -interface Props { - addSuccessToast: (toast: { title: string }) => void; - canUserCreateAndReadCases: () => boolean; - getMarkdownComments: () => string[]; - openCreateCaseFlyout: ({ - comments, - headerContent, - }: { - comments: string[]; - headerContent?: React.ReactNode; - }) => void; -} - -const TakeActionMenuComponent: React.FC = ({ - addSuccessToast, - canUserCreateAndReadCases, - getMarkdownComments, - openCreateCaseFlyout, -}) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const closePopover = useCallback(() => { - setIsPopoverOpen(false); - }, []); - const onButtonClick = useCallback(() => { - setIsPopoverOpen((current) => !current); - }, []); - - const takeActionButton = useMemo( - () => ( - - {i18n.TAKE_ACTION} - - ), - [onButtonClick] - ); - - const { disabled: addToNewCaseDisabled, onAddToNewCase } = useAddToNewCase({ - canUserCreateAndReadCases, - openCreateCaseFlyout, - }); - - const onClickAddToCase = useCallback( - () => onAddToNewCase([getMarkdownComments().join('\n')]), - [getMarkdownComments, onAddToNewCase] - ); - - const onCopy = useCallback(() => { - const markdown = getMarkdownComments().join('\n'); - copyToClipboard(markdown); - - closePopover(); - - addSuccessToast({ - title: COPIED_RESULTS_TOAST_TITLE, - }); - }, [addSuccessToast, closePopover, getMarkdownComments]); - - const addToNewCaseContextMenuOnClick = useCallback(() => { - closePopover(); - onClickAddToCase(); - }, [closePopover, onClickAddToCase]); - - const items = useMemo( - () => [ - - {ADD_TO_NEW_CASE} - , - - - {COPY_TO_CLIPBOARD} - , - ], - [addToNewCaseContextMenuOnClick, addToNewCaseDisabled, onCopy] - ); - - return ( - - - - ); -}; - -TakeActionMenuComponent.displayName = 'TakeActionMenuComponent'; - -export const TakeActionMenu = React.memo(TakeActionMenuComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx new file mode 100644 index 0000000000000..f57d9f52737d7 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { ErrorEmptyPrompt } from '.'; + +describe('ErrorEmptyPrompt', () => { + test('it renders the expected content', () => { + const title = 'This is the title of this work'; + + render( + + + + ); + + expect(screen.getByTestId('errorEmptyPrompt').textContent?.includes(title)).toBe(true); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.tsx index a0b70daee0e40..3214b704dc685 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/error_empty_prompt/index.tsx @@ -15,7 +15,7 @@ interface Props { } const ErrorEmptyPromptComponent: React.FC = ({ title }) => ( - +

{i18n.ERRORS_MAY_OCCUR}

{i18n.THE_FOLLOWING_PRIVILEGES_ARE_REQUIRED} diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx new file mode 100644 index 0000000000000..6efe7579d7325 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 { + IlmExplainLifecycleLifecycleExplain, + IlmExplainLifecycleLifecycleExplainManaged, + IlmExplainLifecycleLifecycleExplainUnmanaged, +} from '@elastic/elasticsearch/lib/api/types'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { IlmPhaseCounts } from '.'; +import { getIlmExplainPhaseCounts } from '../pattern/helpers'; + +const hot: IlmExplainLifecycleLifecycleExplainManaged = { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '3.98d', + lifecycle_date_millis: 1675536751379, + age: '3.98d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, +}; +const warm = { + ...hot, + phase: 'warm', +}; +const cold = { + ...hot, + phase: 'cold', +}; +const frozen = { + ...hot, + phase: 'frozen', +}; + +const managed: Record = { + hot, + warm, + cold, + frozen, +}; + +const unmanaged: IlmExplainLifecycleLifecycleExplainUnmanaged = { + index: 'foo', + managed: false, +}; + +const ilmExplain: Record = { + ...managed, + [unmanaged.index]: unmanaged, +}; + +const ilmExplainPhaseCounts = getIlmExplainPhaseCounts(ilmExplain); + +const pattern = 'packetbeat-*'; + +describe('IlmPhaseCounts', () => { + test('it renders the expected counts', () => { + render( + + + + ); + + expect(screen.getByTestId('ilmPhaseCounts')).toHaveTextContent( + 'hot (1)unmanaged (1)warm (1)cold (1)frozen (1)' + ); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.tsx index 3aa738d4cb788..82664778becb0 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/ilm_phase_counts/index.tsx @@ -25,7 +25,7 @@ interface Props { } const IlmPhaseCountsComponent: React.FC = ({ ilmExplainPhaseCounts, pattern }) => ( - + {phases.map((phase) => ilmExplainPhaseCounts[phase] != null && ilmExplainPhaseCounts[phase] > 0 ? ( diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx new file mode 100644 index 0000000000000..1c37ec799c53c --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EmptyPromptBody } from './empty_prompt_body'; +import { TestProviders } from '../../mock/test_providers/test_providers'; + +describe('EmptyPromptBody', () => { + const content = 'foo bar baz @baz'; + + test('it renders the expected content', () => { + render( + + + + ); + + expect(screen.getByTestId('emptyPromptBody')).toHaveTextContent(content); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.tsx index 80c9151eaa050..33283d11649a8 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_body.tsx @@ -11,7 +11,9 @@ interface Props { body: string; } -const EmptyPromptBodyComponent: React.FC = ({ body }) =>

{body}

; +const EmptyPromptBodyComponent: React.FC = ({ body }) => ( +

{body}

+); EmptyPromptBodyComponent.displayName = 'EmptyPromptBodyComponent'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx new file mode 100644 index 0000000000000..6bb3b72ed3ece --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EmptyPromptTitle } from './empty_prompt_title'; +import { TestProviders } from '../../mock/test_providers/test_providers'; + +describe('EmptyPromptTitle', () => { + const title = 'What is a great title?'; + + test('it renders the expected content', () => { + render( + + + + ); + + expect(screen.getByTestId('emptyPromptTitle')).toHaveTextContent(title); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.tsx index a9d2e1f6a74d9..ee06b2f446858 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/empty_prompt_title.tsx @@ -11,7 +11,9 @@ interface Props { title: string; } -const EmptyPromptTitleComponent: React.FC = ({ title }) =>

{title}

; +const EmptyPromptTitleComponent: React.FC = ({ title }) => ( +

{title}

+); EmptyPromptTitleComponent.displayName = 'EmptyPromptTitleComponent'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.test.ts new file mode 100644 index 0000000000000..e0644fdc4a5c0 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/helpers.test.ts @@ -0,0 +1,800 @@ +/* + * 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 { EcsFlat } from '@kbn/ecs'; + +import { + getMappingsProperties, + getSortedPartitionedFieldMetadata, + hasAllDataFetchingCompleted, +} from './helpers'; +import { mockIndicesGetMappingIndexMappingRecords } from '../../mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record'; +import { mockMappingsProperties } from '../../mock/mappings_properties/mock_mappings_properties'; +import { EcsMetadata } from '../../types'; + +const ecsMetadata: Record = EcsFlat as unknown as Record; + +describe('helpers', () => { + describe('getSortedPartitionedFieldMetadata', () => { + test('it returns null when mappings are loading', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata, + loadingMappings: true, // <-- + mappingsProperties: mockMappingsProperties, + unallowedValues: {}, + }) + ).toBeNull(); + }); + + test('it returns null when `ecsMetadata` is null', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata: null, // <-- + loadingMappings: false, + mappingsProperties: mockMappingsProperties, + unallowedValues: {}, + }) + ).toBeNull(); + }); + + test('it returns null when `unallowedValues` is null', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata, + loadingMappings: false, + mappingsProperties: mockMappingsProperties, + unallowedValues: null, // <-- + }) + ).toBeNull(); + }); + + describe('when `mappingsProperties` is unknown', () => { + const expected = { + all: [], + custom: [], + ecsCompliant: [], + incompatible: [ + { + description: + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', + hasEcsMetadata: true, + indexFieldName: '@timestamp', + indexFieldType: '-', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + type: 'date', + }, + ], + }; + + test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is undefined', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata, + loadingMappings: false, + mappingsProperties: undefined, // <-- + unallowedValues: {}, + }) + ).toEqual(expected); + }); + + test('it returns a `PartitionedFieldMetadata` with an `incompatible` `@timestamp` when `mappingsProperties` is null', () => { + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata, + loadingMappings: false, + mappingsProperties: null, // <-- + unallowedValues: {}, + }) + ).toEqual(expected); + }); + }); + + test('it returns the expected sorted field metadata', () => { + const unallowedValues = { + 'event.category': [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + 'event.kind': [], + 'event.outcome': [], + 'event.type': [], + }; + + expect( + getSortedPartitionedFieldMetadata({ + ecsMetadata, + loadingMappings: false, + mappingsProperties: mockMappingsProperties, + unallowedValues, + }) + ).toEqual({ + all: [ + { + dashed_name: 'timestamp', + description: + 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + flat_name: '@timestamp', + level: 'core', + name: '@timestamp', + normalize: [], + required: true, + short: 'Date/time when the event originated.', + type: 'date', + indexFieldName: '@timestamp', + indexFieldType: 'date', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }, + { + allowed_values: [ + { + description: + 'Events in this category are related to the challenge and response process in which credentials are supplied and verified to allow the creation of a session. Common sources for these logs are Windows event logs and ssh logs. Visualize and analyze events in this category to look for failed logins, and other authentication-related activity.', + expected_event_types: ['start', 'end', 'info'], + name: 'authentication', + }, + { + description: + 'Events in the configuration category have to deal with creating, modifying, or deleting the settings or parameters of an application, process, or system.\nExample sources include security policy change logs, configuration auditing logging, and system integrity monitoring.', + expected_event_types: ['access', 'change', 'creation', 'deletion', 'info'], + name: 'configuration', + }, + { + description: + 'The database category denotes events and metrics relating to a data storage and retrieval system. Note that use of this category is not limited to relational database systems. Examples include event logs from MS SQL, MySQL, Elasticsearch, MongoDB, etc. Use this category to visualize and analyze database activity such as accesses and changes.', + expected_event_types: ['access', 'change', 'info', 'error'], + name: 'database', + }, + { + description: + 'Events in the driver category have to do with operating system device drivers and similar software entities such as Windows drivers, kernel extensions, kernel modules, etc.\nUse events and metrics in this category to visualize and analyze driver-related activity and status on hosts.', + expected_event_types: ['change', 'end', 'info', 'start'], + name: 'driver', + }, + { + description: + 'This category is used for events relating to email messages, email attachments, and email network or protocol activity.\nEmails events can be produced by email security gateways, mail transfer agents, email cloud service providers, or mail server monitoring applications.', + expected_event_types: ['info'], + name: 'email', + }, + { + description: + 'Relating to a set of information that has been created on, or has existed on a filesystem. Use this category of events to visualize and analyze the creation, access, and deletions of files. Events in this category can come from both host-based and network-based sources. An example source of a network-based detection of a file transfer would be the Zeek file.log.', + expected_event_types: ['change', 'creation', 'deletion', 'info'], + name: 'file', + }, + { + description: + 'Use this category to visualize and analyze information such as host inventory or host lifecycle events.\nMost of the events in this category can usually be observed from the outside, such as from a hypervisor or a control plane\'s point of view. Some can also be seen from within, such as "start" or "end".\nNote that this category is for information about hosts themselves; it is not meant to capture activity "happening on a host".', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'host', + }, + { + description: + 'Identity and access management (IAM) events relating to users, groups, and administration. Use this category to visualize and analyze IAM-related logs and data from active directory, LDAP, Okta, Duo, and other IAM systems.', + expected_event_types: [ + 'admin', + 'change', + 'creation', + 'deletion', + 'group', + 'info', + 'user', + ], + name: 'iam', + }, + { + description: + 'Relating to intrusion detections from IDS/IPS systems and functions, both network and host-based. Use this category to visualize and analyze intrusion detection alerts from systems such as Snort, Suricata, and Palo Alto threat detections.', + expected_event_types: ['allowed', 'denied', 'info'], + name: 'intrusion_detection', + }, + { + description: + 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.', + expected_event_types: ['info'], + name: 'malware', + }, + { + description: + 'Relating to all network activity, including network connection lifecycle, network traffic, and essentially any event that includes an IP address. Many events containing decoded network protocol transactions fit into this category. Use events in this category to visualize or analyze counts of network ports, protocols, addresses, geolocation information, etc.', + expected_event_types: [ + 'access', + 'allowed', + 'connection', + 'denied', + 'end', + 'info', + 'protocol', + 'start', + ], + name: 'network', + }, + { + description: + 'Relating to software packages installed on hosts. Use this category to visualize and analyze inventory of software installed on various hosts, or to determine host vulnerability in the absence of vulnerability scan data.', + expected_event_types: [ + 'access', + 'change', + 'deletion', + 'info', + 'installation', + 'start', + ], + name: 'package', + }, + { + description: + 'Use this category of events to visualize and analyze process-specific information such as lifecycle events or process ancestry.', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'process', + }, + { + description: + 'Having to do with settings and assets stored in the Windows registry. Use this category to visualize and analyze activity such as registry access and modifications.', + expected_event_types: ['access', 'change', 'creation', 'deletion'], + name: 'registry', + }, + { + description: + 'The session category is applied to events and metrics regarding logical persistent connections to hosts and services. Use this category to visualize and analyze interactive or automated persistent connections between assets. Data for this category may come from Windows Event logs, SSH logs, or stateless sessions such as HTTP cookie-based sessions, etc.', + expected_event_types: ['start', 'end', 'info'], + name: 'session', + }, + { + description: + "Use this category to visualize and analyze events describing threat actors' targets, motives, or behaviors.", + expected_event_types: ['indicator'], + name: 'threat', + }, + { + description: + 'Relating to vulnerability scan results. Use this category to analyze vulnerabilities detected by Tenable, Qualys, internal scanners, and other vulnerability management sources.', + expected_event_types: ['info'], + name: 'vulnerability', + }, + { + description: + 'Relating to web server access. Use this category to create a dashboard of web server/proxy activity from apache, IIS, nginx web servers, etc. Note: events from network observers such as Zeek http log may also be included in this category.', + expected_event_types: ['access', 'error', 'info'], + name: 'web', + }, + ], + dashed_name: 'event-category', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + flat_name: 'event.category', + ignore_above: 1024, + level: 'core', + name: 'category', + normalize: ['array'], + short: 'Event category. The second categorization field in the hierarchy.', + type: 'keyword', + indexFieldName: 'event.category', + indexFieldType: 'keyword', + indexInvalidValues: [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }, + { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + ignore_above: 1024, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'host.name.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + dashed_name: 'source-ip', + description: 'IP address of the source (IPv4 or IPv6).', + flat_name: 'source.ip', + level: 'core', + name: 'ip', + normalize: [], + short: 'IP address of the source.', + type: 'ip', + indexFieldName: 'source.ip', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'source.ip.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + dashed_name: 'source-port', + description: 'Port of the source.', + flat_name: 'source.port', + format: 'string', + level: 'core', + name: 'port', + normalize: [], + short: 'Port of the source.', + type: 'long', + indexFieldName: 'source.port', + indexFieldType: 'long', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }, + ], + ecsCompliant: [ + { + dashed_name: 'timestamp', + description: + 'Date/time when the event originated.\nThis is the date/time extracted from the event, typically representing when the event was generated by the source.\nIf the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + flat_name: '@timestamp', + level: 'core', + name: '@timestamp', + normalize: [], + required: true, + short: 'Date/time when the event originated.', + type: 'date', + indexFieldName: '@timestamp', + indexFieldType: 'date', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }, + { + dashed_name: 'source-port', + description: 'Port of the source.', + flat_name: 'source.port', + format: 'string', + level: 'core', + name: 'port', + normalize: [], + short: 'Port of the source.', + type: 'long', + indexFieldName: 'source.port', + indexFieldType: 'long', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: true, + isInSameFamily: false, + }, + ], + custom: [ + { + indexFieldName: 'host.name.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'source.ip.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + ], + incompatible: [ + { + allowed_values: [ + { + description: + 'Events in this category are related to the challenge and response process in which credentials are supplied and verified to allow the creation of a session. Common sources for these logs are Windows event logs and ssh logs. Visualize and analyze events in this category to look for failed logins, and other authentication-related activity.', + expected_event_types: ['start', 'end', 'info'], + name: 'authentication', + }, + { + description: + 'Events in the configuration category have to deal with creating, modifying, or deleting the settings or parameters of an application, process, or system.\nExample sources include security policy change logs, configuration auditing logging, and system integrity monitoring.', + expected_event_types: ['access', 'change', 'creation', 'deletion', 'info'], + name: 'configuration', + }, + { + description: + 'The database category denotes events and metrics relating to a data storage and retrieval system. Note that use of this category is not limited to relational database systems. Examples include event logs from MS SQL, MySQL, Elasticsearch, MongoDB, etc. Use this category to visualize and analyze database activity such as accesses and changes.', + expected_event_types: ['access', 'change', 'info', 'error'], + name: 'database', + }, + { + description: + 'Events in the driver category have to do with operating system device drivers and similar software entities such as Windows drivers, kernel extensions, kernel modules, etc.\nUse events and metrics in this category to visualize and analyze driver-related activity and status on hosts.', + expected_event_types: ['change', 'end', 'info', 'start'], + name: 'driver', + }, + { + description: + 'This category is used for events relating to email messages, email attachments, and email network or protocol activity.\nEmails events can be produced by email security gateways, mail transfer agents, email cloud service providers, or mail server monitoring applications.', + expected_event_types: ['info'], + name: 'email', + }, + { + description: + 'Relating to a set of information that has been created on, or has existed on a filesystem. Use this category of events to visualize and analyze the creation, access, and deletions of files. Events in this category can come from both host-based and network-based sources. An example source of a network-based detection of a file transfer would be the Zeek file.log.', + expected_event_types: ['change', 'creation', 'deletion', 'info'], + name: 'file', + }, + { + description: + 'Use this category to visualize and analyze information such as host inventory or host lifecycle events.\nMost of the events in this category can usually be observed from the outside, such as from a hypervisor or a control plane\'s point of view. Some can also be seen from within, such as "start" or "end".\nNote that this category is for information about hosts themselves; it is not meant to capture activity "happening on a host".', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'host', + }, + { + description: + 'Identity and access management (IAM) events relating to users, groups, and administration. Use this category to visualize and analyze IAM-related logs and data from active directory, LDAP, Okta, Duo, and other IAM systems.', + expected_event_types: [ + 'admin', + 'change', + 'creation', + 'deletion', + 'group', + 'info', + 'user', + ], + name: 'iam', + }, + { + description: + 'Relating to intrusion detections from IDS/IPS systems and functions, both network and host-based. Use this category to visualize and analyze intrusion detection alerts from systems such as Snort, Suricata, and Palo Alto threat detections.', + expected_event_types: ['allowed', 'denied', 'info'], + name: 'intrusion_detection', + }, + { + description: + 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.', + expected_event_types: ['info'], + name: 'malware', + }, + { + description: + 'Relating to all network activity, including network connection lifecycle, network traffic, and essentially any event that includes an IP address. Many events containing decoded network protocol transactions fit into this category. Use events in this category to visualize or analyze counts of network ports, protocols, addresses, geolocation information, etc.', + expected_event_types: [ + 'access', + 'allowed', + 'connection', + 'denied', + 'end', + 'info', + 'protocol', + 'start', + ], + name: 'network', + }, + { + description: + 'Relating to software packages installed on hosts. Use this category to visualize and analyze inventory of software installed on various hosts, or to determine host vulnerability in the absence of vulnerability scan data.', + expected_event_types: [ + 'access', + 'change', + 'deletion', + 'info', + 'installation', + 'start', + ], + name: 'package', + }, + { + description: + 'Use this category of events to visualize and analyze process-specific information such as lifecycle events or process ancestry.', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'process', + }, + { + description: + 'Having to do with settings and assets stored in the Windows registry. Use this category to visualize and analyze activity such as registry access and modifications.', + expected_event_types: ['access', 'change', 'creation', 'deletion'], + name: 'registry', + }, + { + description: + 'The session category is applied to events and metrics regarding logical persistent connections to hosts and services. Use this category to visualize and analyze interactive or automated persistent connections between assets. Data for this category may come from Windows Event logs, SSH logs, or stateless sessions such as HTTP cookie-based sessions, etc.', + expected_event_types: ['start', 'end', 'info'], + name: 'session', + }, + { + description: + "Use this category to visualize and analyze events describing threat actors' targets, motives, or behaviors.", + expected_event_types: ['indicator'], + name: 'threat', + }, + { + description: + 'Relating to vulnerability scan results. Use this category to analyze vulnerabilities detected by Tenable, Qualys, internal scanners, and other vulnerability management sources.', + expected_event_types: ['info'], + name: 'vulnerability', + }, + { + description: + 'Relating to web server access. Use this category to create a dashboard of web server/proxy activity from apache, IIS, nginx web servers, etc. Note: events from network observers such as Zeek http log may also be included in this category.', + expected_event_types: ['access', 'error', 'info'], + name: 'web', + }, + ], + dashed_name: 'event-category', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + flat_name: 'event.category', + ignore_above: 1024, + level: 'core', + name: 'category', + normalize: ['array'], + short: 'Event category. The second categorization field in the hierarchy.', + type: 'keyword', + indexFieldName: 'event.category', + indexFieldType: 'keyword', + indexInvalidValues: [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }, + { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + ignore_above: 1024, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + dashed_name: 'source-ip', + description: 'IP address of the source (IPv4 or IPv6).', + flat_name: 'source.ip', + level: 'core', + name: 'ip', + normalize: [], + short: 'IP address of the source.', + type: 'ip', + indexFieldName: 'source.ip', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + ], + }); + }); + }); + + describe('getMappingsProperties', () => { + test('it returns the expected mapping properties', () => { + expect( + getMappingsProperties({ + indexes: mockIndicesGetMappingIndexMappingRecords, + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual({ + '@timestamp': { + type: 'date', + }, + event: { + properties: { + category: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + host: { + properties: { + name: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + }, + }, + some: { + properties: { + field: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + }, + }, + source: { + properties: { + ip: { + fields: { + keyword: { + ignore_above: 256, + type: 'keyword', + }, + }, + type: 'text', + }, + port: { + type: 'long', + }, + }, + }, + }); + }); + + test('it returns null when `indexes` is null', () => { + expect( + getMappingsProperties({ + indexes: null, // <-- + indexName: 'auditbeat-custom-index-1', + }) + ).toBeNull(); + }); + + test('it returns null when `indexName` does not exist in `indexes`', () => { + expect( + getMappingsProperties({ + indexes: mockIndicesGetMappingIndexMappingRecords, + indexName: 'does-not-exist', // <-- + }) + ).toBeNull(); + }); + + test('it returns null when `properties` does not exist in the mappings', () => { + const missingProperties = { + ...mockIndicesGetMappingIndexMappingRecords, + foozle: { + mappings: {}, // <-- does not have a `properties` + }, + }; + + expect( + getMappingsProperties({ + indexes: missingProperties, + indexName: 'foozle', + }) + ).toBeNull(); + }); + }); + + describe('hasAllDataFetchingCompleted', () => { + test('it returns false when both the mappings and unallowed values are loading', () => { + expect( + hasAllDataFetchingCompleted({ + loadingMappings: true, + loadingUnallowedValues: true, + }) + ).toBe(false); + }); + + test('it returns false when mappings are loading, and unallowed values are NOT loading', () => { + expect( + hasAllDataFetchingCompleted({ + loadingMappings: true, + loadingUnallowedValues: false, + }) + ).toBe(false); + }); + + test('it returns false when mappings are NOT loading, and unallowed values are loading', () => { + expect( + hasAllDataFetchingCompleted({ + loadingMappings: false, + loadingUnallowedValues: true, + }) + ).toBe(false); + }); + + test('it returns true when both the mappings and unallowed values have finished loading', () => { + expect( + hasAllDataFetchingCompleted({ + loadingMappings: false, + loadingUnallowedValues: false, + }) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx new file mode 100644 index 0000000000000..b6914daef0e7d --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.test.tsx @@ -0,0 +1,262 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../helpers'; +import { mockMappingsResponse } from '../../mock/mappings_response/mock_mappings_response'; +import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { mockUnallowedValuesResponse } from '../../mock/unallowed_values/mock_unallowed_values'; +import { LOADING_MAPPINGS, LOADING_UNALLOWED_VALUES } from './translations'; +import { UnallowedValueRequestItem } from '../../types'; +import { IndexProperties, Props } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const pattern = 'auditbeat-*'; +const patternRollup = auditbeatWithAllResults; + +let mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + new Promise((resolve) => { + resolve(mockMappingsResponse); // happy path + }) +); + +jest.mock('../../use_mappings/helpers', () => ({ + fetchMappings: ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => + mockFetchMappings({ + abortController, + patternOrIndexName, + }), +})); + +let mockFetchUnallowedValues = jest.fn( + ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => new Promise((resolve) => resolve(mockUnallowedValuesResponse)) +); + +jest.mock('../../use_unallowed_values/helpers', () => { + const original = jest.requireActual('../../use_unallowed_values/helpers'); + + return { + ...original, + fetchUnallowedValues: ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => + mockFetchUnallowedValues({ + abortController, + indexName, + requestItems, + }), + }; +}); + +const defaultProps: Props = { + addSuccessToast: jest.fn(), + canUserCreateAndReadCases: jest.fn(), + docsCount: auditbeatWithAllResults.docsCount ?? 0, + formatBytes, + formatNumber, + getGroupByFieldsOnClick: jest.fn(), + ilmPhase: 'hot', + indexName: 'auditbeat-custom-index-1', + openCreateCaseFlyout: jest.fn(), + pattern, + patternRollup, + theme: DARK_THEME, + updatePatternRollup: jest.fn(), +}; + +describe('IndexProperties', () => { + test('it renders the tab content', async () => { + render( + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('incompatibleTab')).toBeInTheDocument(); + }); + }); + + describe('when an error occurs loading mappings', () => { + const abortController = new AbortController(); + abortController.abort(); + + const error = 'simulated fetch mappings error'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it displays the expected empty prompt content', async () => { + mockFetchMappings = jest.fn( + ({ + // eslint-disable-next-line @typescript-eslint/no-shadow + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => new Promise((_, reject) => reject(new Error(error))) + ); + + render( + + + + ); + + await waitFor(() => { + expect( + screen + .getByTestId('errorEmptyPrompt') + .textContent?.includes('Unable to load index mappings') + ).toBe(true); + }); + }); + }); + + describe('when an error occurs loading unallowed values', () => { + const abortController = new AbortController(); + abortController.abort(); + + const error = 'simulated fetch unallowed values error'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it displays the expected empty prompt content', async () => { + mockFetchUnallowedValues = jest.fn( + ({ + // eslint-disable-next-line @typescript-eslint/no-shadow + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => new Promise((_, reject) => reject(new Error(error))) + ); + + render( + + + + ); + + await waitFor(() => { + expect( + screen + .getByTestId('errorEmptyPrompt') + .textContent?.includes('Unable to load unallowed values') + ).toBe(true); + }); + }); + }); + + describe('when mappings are loading', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it displays the expected loading prompt content', async () => { + mockFetchMappings = jest.fn( + ({ + abortController, + patternOrIndexName, + }: { + abortController: AbortController; + patternOrIndexName: string; + }) => new Promise(() => {}) // <-- will never resolve or reject + ); + + render( + + + + ); + + await waitFor(() => { + expect( + screen.getByTestId('loadingEmptyPrompt').textContent?.includes(LOADING_MAPPINGS) + ).toBe(true); + }); + }); + }); + + describe('when unallowed values are loading', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it displays the expected loading prompt content', async () => { + mockFetchUnallowedValues = jest.fn( + ({ + abortController, + indexName, + requestItems, + }: { + abortController: AbortController; + indexName: string; + requestItems: UnallowedValueRequestItem[]; + }) => new Promise(() => {}) // <-- will never resolve or reject + ); + + render( + + + + ); + + await waitFor(() => { + expect( + screen.getByTestId('loadingEmptyPrompt').textContent?.includes(LOADING_UNALLOWED_VALUES) + ).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx index 34fac241a2d82..ca9eda507f8dd 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/index.tsx @@ -16,7 +16,6 @@ import type { XYChartElementEvent, } from '@elastic/charts'; import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { getUnallowedValueRequestItems } from '../allowed_values/helpers'; @@ -28,8 +27,8 @@ import { hasAllDataFetchingCompleted, INCOMPATIBLE_TAB_ID, } from './helpers'; -import { EMPTY_STAT } from '../../helpers'; import { LoadingEmptyPrompt } from '../loading_empty_prompt'; +import { getIndexPropertiesContainerId } from '../pattern/helpers'; import { getTabs } from '../tabs/helpers'; import { getAllIncompatibleMarkdownComments } from '../tabs/incompatible_tab/helpers'; import * as i18n from './translations'; @@ -40,10 +39,11 @@ import { useUnallowedValues } from '../../use_unallowed_values'; const EMPTY_MARKDOWN_COMMENTS: string[] = []; -interface Props { +export interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; docsCount: number; getGroupByFieldsOnClick: ( elements: Array< @@ -76,7 +76,8 @@ interface Props { const IndexPropertiesComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, docsCount, getGroupByFieldsOnClick, ilmPhase, @@ -87,11 +88,6 @@ const IndexPropertiesComponent: React.FC = ({ theme, updatePatternRollup, }) => { - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); const { error: mappingsError, indexes, loading: loadingMappings } = useMappings(indexName); const requestItems = useMemo( @@ -142,7 +138,8 @@ const IndexPropertiesComponent: React.FC = ({ getTabs({ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, + formatBytes, + formatNumber, docsCount, getGroupByFieldsOnClick, ilmPhase, @@ -152,13 +149,15 @@ const IndexPropertiesComponent: React.FC = ({ pattern, patternDocsCount: patternRollup?.docsCount ?? 0, setSelectedTabId, + stats: patternRollup?.stats ?? null, theme, }), [ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, getGroupByFieldsOnClick, ilmPhase, indexName, @@ -166,6 +165,7 @@ const IndexPropertiesComponent: React.FC = ({ partitionedFieldMetadata, pattern, patternRollup?.docsCount, + patternRollup?.stats, theme, ] ); @@ -212,11 +212,13 @@ const IndexPropertiesComponent: React.FC = ({ partitionedFieldMetadata != null ? getAllIncompatibleMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount: patternRollup.docsCount ?? 0, + sizeInBytes: patternRollup.sizeInBytes, }) : EMPTY_MARKDOWN_COMMENTS; @@ -239,6 +241,7 @@ const IndexPropertiesComponent: React.FC = ({ } }, [ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, @@ -265,10 +268,10 @@ const IndexPropertiesComponent: React.FC = ({ } return indexes != null ? ( - <> +
{renderTabs()} {selectedTabContent} - +
) : null; }; IndexPropertiesComponent.displayName = 'IndexPropertiesComponent'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.test.ts index 0ea7a13d1c710..b51a49ecc89df 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.test.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.test.ts @@ -5,11 +5,244 @@ * 2.0. */ -import { eventCategory, sourceIpWithTextMapping } from '../../../mock/enriched_field_metadata'; +import numeral from '@elastic/numeral'; + +import { + ECS_MAPPING_TYPE_EXPECTED, + FIELD, + INDEX_MAPPING_TYPE_ACTUAL, +} from '../../../compare_fields_table/translations'; +import { ERRORS } from '../../data_quality_summary/errors_popover/translations'; +import { ERROR, INDEX, PATTERN } from '../../data_quality_summary/errors_viewer/translations'; +import { + escape, + escapePreserveNewlines, + getAllowedValues, + getCodeFormattedValue, + getCustomMarkdownTableRows, + getDataQualitySummaryMarkdownComment, + getErrorsMarkdownTable, + getErrorsMarkdownTableRows, + getHeaderSeparator, + getIlmExplainPhaseCountsMarkdownComment, + getIncompatibleMappingsMarkdownTableRows, + getIncompatibleValuesMarkdownTableRows, + getIndexInvalidValues, + getMarkdownComment, + getMarkdownTable, + getMarkdownTableHeader, + getPatternSummaryMarkdownComment, + getResultEmoji, + getSameFamilyBadge, + getStatsRollupMarkdownComment, + getSummaryMarkdownComment, + getSummaryTableMarkdownComment, + getSummaryTableMarkdownHeader, + getSummaryTableMarkdownRow, + getTabCountsMarkdownComment, +} from './helpers'; +import { EMPTY_STAT } from '../../../helpers'; +import { mockAllowedValues } from '../../../mock/allowed_values/mock_allowed_values'; +import { + eventCategory, + mockCustomFields, + mockIncompatibleMappings, + sourceIpWithTextMapping, +} from '../../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { + auditbeatNoResults, + auditbeatWithAllResults, +} from '../../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; import { SAME_FAMILY } from '../../same_family/translations'; -import { getIncompatibleMappingsMarkdownTableRows, getSameFamilyBadge } from './helpers'; +import { INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE } from '../../tabs/incompatible_tab/translations'; +import { + EnrichedFieldMetadata, + ErrorSummary, + PatternRollup, + UnallowedValueCount, +} from '../../../types'; + +const errorSummary: ErrorSummary[] = [ + { + pattern: '.alerts-security.alerts-default', + indexName: null, + error: 'Error loading stats: Error: Forbidden', + }, + { + error: + 'Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden', + indexName: 'auditbeat-7.2.1-2023.02.13-000001', + pattern: 'auditbeat-*', + }, +]; + +const indexName = 'auditbeat-custom-index-1'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; describe('helpers', () => { + describe('escape', () => { + test('it returns undefined when `content` is undefined', () => { + expect(escape(undefined)).toBeUndefined(); + }); + + test("it returns the content unmodified when there's nothing to escape", () => { + const content = "there's nothing to escape in this content"; + expect(escape(content)).toEqual(content); + }); + + test('it replaces all newlines in the content with spaces', () => { + const content = '\nthere were newlines in the beginning, middle,\nand end\n'; + expect(escape(content)).toEqual(' there were newlines in the beginning, middle, and end '); + }); + + test('it escapes all column separators in the content with spaces', () => { + const content = '|there were column separators in the beginning, middle,|and end|'; + expect(escape(content)).toEqual( + '\\|there were column separators in the beginning, middle,\\|and end\\|' + ); + }); + + test('it escapes content containing BOTH newlines and column separators', () => { + const content = + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; + expect(escape(content)).toEqual( + '\\| there were newlines and column separators in the beginning, middle, \\|and end\\| ' + ); + }); + }); + + describe('escapePreserveNewlines', () => { + test('it returns undefined when `content` is undefined', () => { + expect(escapePreserveNewlines(undefined)).toBeUndefined(); + }); + + test("it returns the content unmodified when there's nothing to escape", () => { + const content = "there's (also) nothing to escape in this content"; + expect(escapePreserveNewlines(content)).toEqual(content); + }); + + test('it escapes all column separators in the content with spaces', () => { + const content = '|there were column separators in the beginning, middle,|and end|'; + expect(escapePreserveNewlines(content)).toEqual( + '\\|there were column separators in the beginning, middle,\\|and end\\|' + ); + }); + + test('it does NOT escape newlines in the content', () => { + const content = + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; + expect(escapePreserveNewlines(content)).toEqual( + '\\|\nthere were newlines and column separators in the beginning, middle,\n\\|and end\\|\n' + ); + }); + }); + + describe('getHeaderSeparator', () => { + test('it returns a sequence of dashes equal to the length of the header, plus two additional dashes to pad each end of the cntent', () => { + const content = '0123456789'; // content.length === 10 + const expected = '------------'; // expected.length === 12 + + expect(getHeaderSeparator(content)).toEqual(expected); + }); + }); + + describe('getMarkdownTableHeader', () => { + const headerNames = [ + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n', + 'A second column', + 'A third column', + ]; + + test('it returns the expected table header', () => { + expect(getMarkdownTableHeader(headerNames)).toEqual( + '\n| \\| there were newlines and column separators in the beginning, middle, \\|and end\\| | A second column | A third column | \n|----------------------------------------------------------------------------------|-----------------|----------------|' + ); + }); + }); + + describe('getCodeFormattedValue', () => { + test('it returns the expected placeholder when `value` is undefined', () => { + expect(getCodeFormattedValue(undefined)).toEqual('`--`'); + }); + + test('it returns the content formatted as markdown code', () => { + const value = 'foozle'; + + expect(getCodeFormattedValue(value)).toEqual('`foozle`'); + }); + + test('it escapes content such that `value` may be included in a markdown table cell', () => { + const value = + '|\nthere were newlines and column separators in the beginning, middle,\n|and end|\n'; + + expect(getCodeFormattedValue(value)).toEqual( + '`\\| there were newlines and column separators in the beginning, middle, \\|and end\\| `' + ); + }); + }); + + describe('getAllowedValues', () => { + test('it returns the expected placeholder when `allowedValues` is undefined', () => { + expect(getAllowedValues(undefined)).toEqual('`--`'); + }); + + test('it joins the `allowedValues` `name`s as a markdown-code-formatted, comma separated, string', () => { + expect(getAllowedValues(mockAllowedValues)).toEqual( + '`authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web`' + ); + }); + }); + + describe('getIndexInvalidValues', () => { + test('it returns the expected placeholder when `indexInvalidValues` is empty', () => { + expect(getIndexInvalidValues([])).toEqual('`--`'); + }); + + test('it returns markdown-code-formatted `fieldName`s, and their associated `count`s', () => { + const indexInvalidValues: UnallowedValueCount[] = [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ]; + + expect(getIndexInvalidValues(indexInvalidValues)).toEqual( + `\`an_invalid_category\` (2), \`theory\` (1)` + ); + }); + }); + + describe('getCustomMarkdownTableRows', () => { + test('it returns the expected table rows', () => { + expect(getCustomMarkdownTableRows(mockCustomFields)).toEqual( + '| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |' + ); + }); + + test('it returns the expected table rows when some have allowed values', () => { + const withAllowedValues = [ + ...mockCustomFields, + eventCategory, // note: this is not a real-world use case, because custom fields don't have allowed values + ]; + + expect(getCustomMarkdownTableRows(withAllowedValues)).toEqual( + '| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n| event.category | `keyword` | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` |' + ); + }); + }); + describe('getSameFamilyBadge', () => { test('it returns the expected badge text when the field is in the same family', () => { const inSameFamily = { @@ -32,7 +265,7 @@ describe('helpers', () => { describe('getIncompatibleMappingsMarkdownTableRows', () => { test('it returns the expected table rows when the field is in the same family', () => { - const eventCategoryWithWildcard = { + const eventCategoryWithWildcard: EnrichedFieldMetadata = { ...eventCategory, // `event.category` is a `keyword` per the ECS spec indexFieldType: 'wildcard', // this index has a mapping of `wildcard` instead of `keyword` isInSameFamily: true, // `wildcard` and `keyword` are in the same family @@ -49,18 +282,367 @@ describe('helpers', () => { }); test('it returns the expected table rows when the field is NOT in the same family', () => { - const eventCategoryWithWildcard = { + const eventCategoryWithText: EnrichedFieldMetadata = { ...eventCategory, // `event.category` is a `keyword` per the ECS spec indexFieldType: 'text', // this index has a mapping of `text` instead of `keyword` isInSameFamily: false, // `text` and `keyword` are NOT in the same family }; expect( - getIncompatibleMappingsMarkdownTableRows([ - eventCategoryWithWildcard, - sourceIpWithTextMapping, - ]) + getIncompatibleMappingsMarkdownTableRows([eventCategoryWithText, sourceIpWithTextMapping]) ).toEqual('| event.category | `keyword` | `text` |\n| source.ip | `ip` | `text` |'); }); }); + + describe('getIncompatibleValuesMarkdownTableRows', () => { + test('it returns the expected table rows', () => { + expect( + getIncompatibleValuesMarkdownTableRows([ + { + ...eventCategory, + hasEcsMetadata: true, + indexInvalidValues: [ + { + count: 2, + fieldName: 'an_invalid_category', + }, + { + count: 1, + fieldName: 'theory', + }, + ], + isEcsCompliant: false, + }, + ]) + ).toEqual( + '| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |' + ); + }); + }); + + describe('getMarkdownComment', () => { + test('it returns the expected markdown comment', () => { + const suggestedAction = + '|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n|and end|\n'; + const title = + '|\nthere were newlines and column separators in this title beginning, middle,\n|and end|\n'; + + expect(getMarkdownComment({ suggestedAction, title })).toEqual( + '#### \\| there were newlines and column separators in this title beginning, middle, \\|and end\\| \n\n\\|\nthere were newlines and column separators in this suggestedAction beginning, middle,\n\\|and end\\|\n' + ); + }); + }); + + describe('getErrorsMarkdownTableRows', () => { + test('it returns the expected markdown table rows', () => { + expect(getErrorsMarkdownTableRows(errorSummary)).toEqual( + '| .alerts-security.alerts-default | -- | `Error loading stats: Error: Forbidden` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | `Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden` |' + ); + }); + }); + + describe('getErrorsMarkdownTable', () => { + test('it returns the expected table contents', () => { + expect( + getErrorsMarkdownTable({ + errorSummary, + getMarkdownTableRows: getErrorsMarkdownTableRows, + headerNames: [PATTERN, INDEX, ERROR], + title: ERRORS, + }) + ).toEqual( + `## Errors\n\nSome indices were not checked for Data Quality\n\nErrors may occur when pattern or index metadata is temporarily unavailable, or because you don't have the privileges required for access\n\nThe following privileges are required to check an index:\n- \`monitor\` or \`manage\`\n- \`view_index_metadata\`\n- \`read\`\n\n\n| Pattern | Index | Error | \n|---------|-------|-------|\n| .alerts-security.alerts-default | -- | \`Error loading stats: Error: Forbidden\` |\n| auditbeat-* | auditbeat-7.2.1-2023.02.13-000001 | \`Error: Error loading unallowed values for index auditbeat-7.2.1-2023.02.13-000001: Forbidden\` |\n` + ); + }); + + test('it returns an empty string when the error summary is empty', () => { + expect( + getErrorsMarkdownTable({ + errorSummary: [], // <-- empty + getMarkdownTableRows: getErrorsMarkdownTableRows, + headerNames: [PATTERN, INDEX, ERROR], + title: ERRORS, + }) + ).toEqual(''); + }); + }); + + describe('getMarkdownTable', () => { + test('it returns the expected table contents', () => { + expect( + getMarkdownTable({ + enrichedFieldMetadata: mockIncompatibleMappings, + getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + ).toEqual( + '#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n' + ); + }); + + test('it returns an empty string when `enrichedFieldMetadata` is empty', () => { + expect( + getMarkdownTable({ + enrichedFieldMetadata: [], // <-- empty + getMarkdownTableRows: getIncompatibleMappingsMarkdownTableRows, + headerNames: [FIELD, ECS_MAPPING_TYPE_EXPECTED, INDEX_MAPPING_TYPE_ACTUAL], + title: INCOMPATIBLE_FIELD_MAPPINGS_TABLE_TITLE(indexName), + }) + ).toEqual(''); + }); + }); + + describe('getSummaryMarkdownComment', () => { + test('it returns the expected markdown comment', () => { + expect(getSummaryMarkdownComment(indexName)).toEqual('### auditbeat-custom-index-1\n'); + }); + }); + + describe('getTabCountsMarkdownComment', () => { + test('it returns a comment with the expected counts', () => { + expect(getTabCountsMarkdownComment(mockPartitionedFieldMetadata)).toBe( + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n' + ); + }); + }); + + describe('getResultEmoji', () => { + test('it returns the expected placeholder when `incompatible` is undefined', () => { + expect(getResultEmoji(undefined)).toEqual('--'); + }); + + test('it returns a ✅ when the incompatible count is zero', () => { + expect(getResultEmoji(0)).toEqual('✅'); + }); + + test('it returns a ❌ when the incompatible count is NOT zero', () => { + expect(getResultEmoji(1)).toEqual('❌'); + }); + }); + + describe('getSummaryTableMarkdownHeader', () => { + test('it returns the expected header', () => { + expect(getSummaryTableMarkdownHeader()).toEqual( + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|' + ); + }); + }); + + describe('getSummaryTableMarkdownRow', () => { + test('it returns the expected row when all values are provided', () => { + expect( + getSummaryTableMarkdownRow({ + docsCount: 4, + formatBytes, + formatNumber, + incompatible: 3, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual('| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n'); + }); + + test('it returns the expected row when optional values are NOT provided', () => { + expect( + getSummaryTableMarkdownRow({ + docsCount: 4, + formatBytes, + formatNumber, + incompatible: undefined, // <-- + ilmPhase: undefined, // <-- + indexName: 'auditbeat-custom-index-1', + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- | -- | 27.7KB |\n'); + }); + }); + + describe('getSummaryTableMarkdownComment', () => { + test('it returns the expected comment', () => { + expect( + getSummaryTableMarkdownComment({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual( + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n' + ); + }); + }); + + describe('getStatsRollupMarkdownComment', () => { + test('it returns the expected comment', () => { + expect( + getStatsRollupMarkdownComment({ + docsCount: 57410, + formatBytes, + formatNumber, + incompatible: 3, + indices: 25, + indicesChecked: 1, + sizeInBytes: 28413, + }) + ).toEqual( + '| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 3 | 1 | 25 | 27.7KB | 57,410 |\n' + ); + }); + + test('it returns the expected comment when optional values are undefined', () => { + expect( + getStatsRollupMarkdownComment({ + docsCount: 0, + formatBytes, + formatNumber, + incompatible: undefined, + indices: undefined, + indicesChecked: undefined, + sizeInBytes: undefined, + }) + ).toEqual( + '| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| -- | -- | -- | -- | 0 |\n' + ); + }); + }); + + describe('getDataQualitySummaryMarkdownComment', () => { + test('it returns the expected comment', () => { + expect( + getDataQualitySummaryMarkdownComment({ + formatBytes, + formatNumber, + totalDocsCount: 3343719, + totalIncompatible: 4, + totalIndices: 30, + totalIndicesChecked: 2, + sizeInBytes: 4294967296, + }) + ).toEqual( + '# Data quality\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 2 | 30 | 4GB | 3,343,719 |\n\n' + ); + }); + + test('it returns the expected comment when optional values are undefined', () => { + expect( + getDataQualitySummaryMarkdownComment({ + formatBytes, + formatNumber, + totalDocsCount: undefined, + totalIncompatible: undefined, + totalIndices: undefined, + totalIndicesChecked: undefined, + sizeInBytes: undefined, + }) + ).toEqual( + '# Data quality\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| -- | -- | -- | -- | 0 |\n\n' + ); + }); + }); + + describe('getIlmExplainPhaseCountsMarkdownComment', () => { + test('it returns the expected comment when _all_ of the counts are greater than zero', () => { + expect( + getIlmExplainPhaseCountsMarkdownComment({ + hot: 99, + warm: 8, + unmanaged: 77, + cold: 6, + frozen: 55, + }) + ).toEqual('`hot(99)` `warm(8)` `unmanaged(77)` `cold(6)` `frozen(55)`'); + }); + + test('it returns the expected comment when _some_ of the counts are greater than zero', () => { + expect( + getIlmExplainPhaseCountsMarkdownComment({ + hot: 9, + warm: 0, + unmanaged: 2, + cold: 1, + frozen: 0, + }) + ).toEqual('`hot(9)` `unmanaged(2)` `cold(1)`'); + }); + + test('it returns the expected comment when _none_ of the counts are greater than zero', () => { + expect( + getIlmExplainPhaseCountsMarkdownComment({ + hot: 0, + warm: 0, + unmanaged: 0, + cold: 0, + frozen: 0, + }) + ).toEqual(''); + }); + }); + + describe('getPatternSummaryMarkdownComment', () => { + test('it returns the expected comment when the rollup contains results for all of the indices in the pattern', () => { + expect( + getPatternSummaryMarkdownComment({ + formatBytes, + formatNumber, + patternRollup: auditbeatWithAllResults, + }) + ).toEqual( + '## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n' + ); + }); + + test('it returns the expected comment when the rollup contains no results', () => { + expect( + getPatternSummaryMarkdownComment({ + formatBytes, + formatNumber, + patternRollup: auditbeatNoResults, + }) + ).toEqual( + '## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| -- | 0 | 3 | 17.9MB | 19,127 |\n\n' + ); + }); + + test('it returns the expected comment when the rollup does NOT have `ilmExplainPhaseCounts`', () => { + const noIlmExplainPhaseCounts: PatternRollup = { + ...auditbeatWithAllResults, + ilmExplainPhaseCounts: undefined, // <-- + }; + + expect( + getPatternSummaryMarkdownComment({ + formatBytes, + formatNumber, + patternRollup: noIlmExplainPhaseCounts, + }) + ).toEqual( + '## auditbeat-*\n\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 19,127 |\n\n' + ); + }); + + test('it returns the expected comment when `docsCount` is undefined', () => { + const noDocsCount: PatternRollup = { + ...auditbeatWithAllResults, + docsCount: undefined, // <-- + }; + + expect( + getPatternSummaryMarkdownComment({ + formatBytes, + formatNumber, + patternRollup: noDocsCount, + }) + ).toEqual( + '## auditbeat-*\n`hot(1)` `unmanaged(2)`\n\n| Incompatible fields | Indices checked | Indices | Size | Docs |\n|---------------------|-----------------|---------|------|------|\n| 4 | 3 | 3 | 17.9MB | 0 |\n\n' + ); + }); + }); }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.ts index 9137bb346d660..32424dff66883 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/index_properties/markdown/helpers.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { repeat } from 'lodash/fp'; - import { ERRORS_MAY_OCCUR, ERRORS_CALLOUT_SUMMARY, @@ -44,6 +42,7 @@ import { INDICES, INDICES_CHECKED, RESULT, + SIZE, } from '../../summary_table/translations'; import { DATA_QUALITY_TITLE } from '../../../translations'; @@ -65,11 +64,11 @@ export const escape = (content: string | undefined): string | undefined => export const escapePreserveNewlines = (content: string | undefined): string | undefined => content != null ? content.replaceAll('|', '\\|') : content; -export const getHeaderSeparator = (headerLength: number): string => repeat(headerLength + 2, '-'); +export const getHeaderSeparator = (headerText: string): string => '-'.repeat(headerText.length + 2); // 2 extra, for the spaces on both sides of the column name export const getMarkdownTableHeader = (headerNames: string[]) => ` | ${headerNames.map((name) => `${escape(name)} | `).join('')} -|${headerNames.map((name) => `${getHeaderSeparator(name.length)}|`).join('')}`; +|${headerNames.map((name) => `${getHeaderSeparator(name)}|`).join('')}`; export const getCodeFormattedValue = (value: string | undefined) => `\`${escape(value ?? EMPTY_PLACEHOLDER)}\``; @@ -84,21 +83,7 @@ export const getIndexInvalidValues = (indexInvalidValues: UnallowedValueCount[]) ? getCodeFormattedValue(undefined) : indexInvalidValues .map(({ fieldName, count }) => `${getCodeFormattedValue(escape(fieldName))} (${count})`) - .join(',\n'); - -export const getCommonMarkdownTableRows = ( - enrichedFieldMetadata: EnrichedFieldMetadata[] -): string => - enrichedFieldMetadata - .map( - (x) => - `| ${escape(x.indexFieldName)} | ${getCodeFormattedValue(x.type)} | ${getCodeFormattedValue( - x.indexFieldType - )} | ${getAllowedValues(x.allowed_values)} | ${getIndexInvalidValues( - x.indexInvalidValues - )} | ${escape(x.description ?? EMPTY_PLACEHOLDER)} |` - ) - .join('\n'); + .join(', '); // newlines are instead joined with spaces export const getCustomMarkdownTableRows = ( enrichedFieldMetadata: EnrichedFieldMetadata[] @@ -207,19 +192,7 @@ ${getMarkdownTableRows(enrichedFieldMetadata)} ` : ''; -export const getSummaryMarkdownComment = ({ - ecsFieldReferenceUrl, - ecsReferenceUrl, - incompatible, - indexName, - mappingUrl, -}: { - ecsFieldReferenceUrl: string; - ecsReferenceUrl: string; - incompatible: number | undefined; - indexName: string; - mappingUrl: string; -}): string => +export const getSummaryMarkdownComment = (indexName: string) => `### ${escape(indexName)} `; @@ -244,23 +217,31 @@ export const getResultEmoji = (incompatible: number | undefined): string => { }; export const getSummaryTableMarkdownHeader = (): string => - `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE} | -|--------|-------|------|---------------------|-----------|`; + `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE} | ${SIZE} | +|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator( + DOCS + )}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator( + ILM_PHASE + )}|${getHeaderSeparator(SIZE)}|`; export const getSummaryTableMarkdownRow = ({ docsCount, + formatBytes, formatNumber, ilmPhase, incompatible, indexName, patternDocsCount, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; incompatible: number | undefined; indexName: string; patternDocsCount: number; + sizeInBytes: number | undefined; }): string => `| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber( docsCount @@ -268,77 +249,95 @@ export const getSummaryTableMarkdownRow = ({ docsCount, patternDocsCount, })}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${ - ilmPhase != null ? getCodeFormattedValue(ilmPhase) : '' - } | + ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER + } | ${formatBytes(sizeInBytes)} | `; export const getSummaryTableMarkdownComment = ({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; + sizeInBytes: number | undefined; }): string => `${getSummaryTableMarkdownHeader()} ${getSummaryTableMarkdownRow({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, incompatible: partitionedFieldMetadata.incompatible.length, patternDocsCount, + sizeInBytes, })} `; export const getStatsRollupMarkdownComment = ({ docsCount, + formatBytes, formatNumber, incompatible, indices, indicesChecked, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; incompatible: number | undefined; indices: number | undefined; indicesChecked: number | undefined; + sizeInBytes: number | undefined; }): string => - `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${DOCS} | -|---------------------|-----------------|---------|------| + `| ${INCOMPATIBLE_FIELDS} | ${INDICES_CHECKED} | ${INDICES} | ${SIZE} | ${DOCS} | +|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator( + INDICES_CHECKED + )}|${getHeaderSeparator(INDICES)}|${getHeaderSeparator(SIZE)}|${getHeaderSeparator(DOCS)}| | ${incompatible ?? EMPTY_STAT} | ${indicesChecked ?? EMPTY_STAT} | ${ indices ?? EMPTY_STAT - } | ${formatNumber(docsCount)} | + } | ${formatBytes(sizeInBytes)} | ${formatNumber(docsCount)} | `; export const getDataQualitySummaryMarkdownComment = ({ + formatBytes, formatNumber, totalDocsCount, totalIncompatible, totalIndices, totalIndicesChecked, + sizeInBytes, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; totalDocsCount: number | undefined; totalIncompatible: number | undefined; totalIndices: number | undefined; totalIndicesChecked: number | undefined; + sizeInBytes: number | undefined; }): string => `# ${DATA_QUALITY_TITLE} ${getStatsRollupMarkdownComment({ docsCount: totalDocsCount ?? 0, + formatBytes, formatNumber, incompatible: totalIncompatible, indices: totalIndices, indicesChecked: totalIndicesChecked, + sizeInBytes, })} `; @@ -355,13 +354,17 @@ export const getIlmExplainPhaseCountsMarkdownComment = ({ unmanaged > 0 ? getCodeFormattedValue(`${UNMANAGED}(${unmanaged})`) : '', cold > 0 ? getCodeFormattedValue(`${COLD}(${cold})`) : '', frozen > 0 ? getCodeFormattedValue(`${FROZEN}(${frozen})`) : '', - ].join(' '); + ] + .filter((x) => x !== '') // prevents extra whitespace + .join(' '); export const getPatternSummaryMarkdownComment = ({ + formatBytes, formatNumber, patternRollup, patternRollup: { docsCount, indices, ilmExplainPhaseCounts, pattern, results }, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; patternRollup: PatternRollup; }): string => @@ -374,9 +377,11 @@ ${ ${getStatsRollupMarkdownComment({ docsCount: docsCount ?? 0, + formatBytes, formatNumber, incompatible: getTotalPatternIncompatible(results), indices, indicesChecked: getTotalPatternIndicesChecked(patternRollup), + sizeInBytes: patternRollup.sizeInBytes, })} `; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/loading_empty_prompt/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/loading_empty_prompt/index.tsx index 84e701c0aa244..7eda6d039ffe0 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/loading_empty_prompt/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/loading_empty_prompt/index.tsx @@ -15,7 +15,9 @@ interface Props { const LoadingEmptyPromptComponent: React.FC = ({ loading }) => { const icon = useMemo(() => , []); - return {loading}} />; + return ( + {loading}} /> + ); }; LoadingEmptyPromptComponent.displayName = 'LoadingEmptyPromptComponent'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/panel_subtitle/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/panel_subtitle/index.tsx deleted file mode 100644 index 10e460d9c24f5..0000000000000 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/panel_subtitle/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { EuiCode, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../../translations'; - -interface Props { - error: string | null; - loading: boolean; - version: string | null; - versionLoading: boolean; -} - -const PanelSubtitleComponent: React.FC = ({ error, loading, version, versionLoading }) => { - const allDataLoaded = !loading && !versionLoading && error == null && version != null; - - return allDataLoaded ? ( - - - - {i18n.SELECT_AN_INDEX} {version} - - - - ) : null; -}; - -export const PanelSubtitle = React.memo(PanelSubtitleComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts new file mode 100644 index 0000000000000..c40f6a73ffa03 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.test.ts @@ -0,0 +1,865 @@ +/* + * 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 { + IlmExplainLifecycleLifecycleExplain, + IlmExplainLifecycleLifecycleExplainManaged, + IlmExplainLifecycleLifecycleExplainUnmanaged, +} from '@elastic/elasticsearch/lib/api/types'; + +import { + defaultSort, + getIlmPhase, + getIndexPropertiesContainerId, + getIlmExplainPhaseCounts, + getIndexIncompatible, + getPageIndex, + getPhaseCount, + getSummaryTableItems, + isManaged, + shouldCreateIndexNames, + shouldCreatePatternRollup, +} from './helpers'; +import { mockIlmExplain } from '../../mock/ilm_explain/mock_ilm_explain'; +import { mockDataQualityCheckResult } from '../../mock/data_quality_check_result/mock_index'; +import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { mockStats } from '../../mock/stats/mock_stats'; +import { IndexSummaryTableItem } from '../summary_table/helpers'; +import { DataQualityCheckResult } from '../../types'; + +const hot: IlmExplainLifecycleLifecycleExplainManaged = { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '3.98d', + lifecycle_date_millis: 1675536751379, + age: '3.98d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, +}; +const warm = { + ...hot, + phase: 'warm', +}; +const cold = { + ...hot, + phase: 'cold', +}; +const frozen = { + ...hot, + phase: 'frozen', +}; +const other = { + ...hot, + phase: 'other', // not a valid phase +}; + +const managed: Record = { + hot, + warm, + cold, + frozen, +}; + +const unmanaged: IlmExplainLifecycleLifecycleExplainUnmanaged = { + index: 'michael', + managed: false, +}; + +describe('helpers', () => { + const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + + describe('isManaged', () => { + test('it returns true when the `ilmExplainRecord` `managed` property is true', () => { + const ilmExplain = mockIlmExplain[indexName]; + + expect(isManaged(ilmExplain)).toBe(true); + }); + + test('it returns false when the `ilmExplainRecord` is undefined', () => { + expect(isManaged(undefined)).toBe(false); + }); + }); + + describe('getPhaseCount', () => { + test('it returns the expected count when an index with the specified `ilmPhase` exists in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'hot', // this phase is in the record + indexName, // valid index name + }) + ).toEqual(1); + }); + + test('it returns zero when `ilmPhase` is null', () => { + expect( + getPhaseCount({ + ilmExplain: null, + ilmPhase: 'hot', + indexName, + }) + ).toEqual(0); + }); + + test('it returns zero when the `indexName` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'hot', + indexName: 'invalid', // this index does NOT exist + }) + ).toEqual(0); + }); + + test('it returns zero when the specified `ilmPhase` does NOT exist in the `IlmExplainLifecycleLifecycleExplain` record', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'warm', // this phase is NOT in the record + indexName, // valid index name + }) + ).toEqual(0); + }); + + describe('when `ilmPhase` is `unmanaged`', () => { + test('it returns the expected count for an `unmanaged` index', () => { + const index = 'auditbeat-custom-index-1'; + const ilmExplainRecord: IlmExplainLifecycleLifecycleExplain = { + index, + managed: false, + }; + const ilmExplain = { + [index]: ilmExplainRecord, + }; + + expect( + getPhaseCount({ + ilmExplain, + ilmPhase: 'unmanaged', // ilmPhase is unmanaged + indexName: index, // an unmanaged index + }) + ).toEqual(1); + }); + + test('it returns zero for a managed index', () => { + expect( + getPhaseCount({ + ilmExplain: mockIlmExplain, + ilmPhase: 'unmanaged', // ilmPhase is unmanaged + indexName, // a managed (`hot`) index + }) + ).toEqual(0); + }); + }); + }); + + describe('getIlmPhase', () => { + test('it returns undefined when the `ilmExplainRecord` is undefined', () => { + expect(getIlmPhase(undefined)).toBeUndefined(); + }); + + describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainManaged` record', () => { + Object.keys(managed).forEach((phase) => + test(`it returns the expected phase when 'phase' is '${phase}'`, () => { + expect(getIlmPhase(managed[phase])).toEqual(phase); + }) + ); + + test(`it returns undefined when the 'phase' is unknown`, () => { + expect(getIlmPhase(other)).toBeUndefined(); + }); + }); + + describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainUnmanaged` record', () => { + test('it returns `unmanaged`', () => { + expect(getIlmPhase(unmanaged)).toEqual('unmanaged'); + }); + }); + }); + + describe('getIlmExplainPhaseCounts', () => { + test('it returns the expected counts (all zeros) when `ilmExplain` is null', () => { + expect(getIlmExplainPhaseCounts(null)).toEqual({ + cold: 0, + frozen: 0, + hot: 0, + unmanaged: 0, + warm: 0, + }); + }); + + test('it returns the expected counts', () => { + const ilmExplain: Record = { + ...managed, + [unmanaged.index]: unmanaged, + }; + + expect(getIlmExplainPhaseCounts(ilmExplain)).toEqual({ + cold: 1, + frozen: 1, + hot: 1, + unmanaged: 1, + warm: 1, + }); + }); + }); + + describe('getIndexIncompatible', () => { + test('it returns undefined when `results` is undefined', () => { + expect( + getIndexIncompatible({ + indexName, + results: undefined, // <-- + }) + ).toBeUndefined(); + }); + + test('it returns undefined when `indexName` is not in the `results`', () => { + expect( + getIndexIncompatible({ + indexName: 'not_in_the_results', // <-- + results: mockDataQualityCheckResult, + }) + ).toBeUndefined(); + }); + + test('it returns the expected count', () => { + expect( + getIndexIncompatible({ + indexName: 'auditbeat-custom-index-1', + results: mockDataQualityCheckResult, + }) + ).toEqual(3); + }); + }); + + describe('getSummaryTableItems', () => { + const indexNames = [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + 'auditbeat-custom-index-1', + ]; + const pattern = 'auditbeat-*'; + const patternDocsCount = 4; + const results: Record = { + 'auditbeat-custom-index-1': { + docsCount: 4, + error: null, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + markdownComments: [ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2),\n`theory` (1) |\n\n', + ], + pattern: 'auditbeat-*', + }, + }; + + test('it returns the expected summary table items', () => { + expect( + getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames, + pattern, + patternDocsCount, + results, + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: mockStats, + }) + ).toEqual([ + { + docsCount: 1630289, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 733175040, + }, + { + docsCount: 1628343, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 731583142, + }, + { + docsCount: 4, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 28413, + }, + ]); + }); + + test('it returns the expected summary table items when `sortByDirection` is ascending', () => { + expect( + getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames, + pattern, + patternDocsCount, + results, + sortByColumn: defaultSort.sort.field, + sortByDirection: 'asc', // <-- ascending + stats: mockStats, + }) + ).toEqual([ + { + docsCount: 4, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 28413, + }, + { + docsCount: 1628343, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 731583142, + }, + { + docsCount: 1630289, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 733175040, + }, + ]); + }); + + test('it returns the expected summary table items when data is unavailable', () => { + expect( + getSummaryTableItems({ + ilmExplain: null, // <-- no data + indexNames, + pattern, + patternDocsCount, + results: undefined, // <-- no data + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: null, // <-- no data + }) + ).toEqual([ + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 0, + }, + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 0, + }, + { + docsCount: 0, + ilmPhase: undefined, + incompatible: undefined, + indexName: 'auditbeat-custom-index-1', + pattern: 'auditbeat-*', + patternDocsCount: 4, + sizeInBytes: 0, + }, + ]); + }); + }); + + describe('shouldCreateIndexNames', () => { + const indexNames = [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + 'auditbeat-custom-index-1', + ]; + + test('returns true when `indexNames` does NOT exist, and the required `stats` and `ilmExplain` are available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('returns false when `indexNames` exists, and the required `stats` and `ilmExplain` are available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames, + stats: mockStats, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: mockIlmExplain, + indexNames: undefined, + stats: null, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames: undefined, + stats: mockStats, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` does NOT exist, `stats` is NOT available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames: undefined, + stats: null, + }) + ).toBe(false); + }); + + test('returns false when `indexNames` exists, `stats` is NOT available, and `ilmExplain` is NOT available', () => { + expect( + shouldCreateIndexNames({ + ilmExplain: null, + indexNames, + stats: null, + }) + ).toBe(false); + }); + }); + + describe('shouldCreatePatternRollup', () => { + test('it returns false when the `patternRollup` already exists', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + patternRollup: auditbeatWithAllResults, + stats: mockStats, + }) + ).toBe(false); + }); + + test('it returns true when all data was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('it returns false when `stats`, but NOT `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: null, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(false); + }); + + test('it returns false when `stats` was NOT loaded, and `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: null, + ilmExplain: mockIlmExplain, + patternRollup: undefined, + stats: null, + }) + ).toBe(false); + }); + + test('it returns true if an error occurred, and NO data was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'whoops', + ilmExplain: null, + patternRollup: undefined, + stats: null, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and just `stats` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'something went', + ilmExplain: null, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and just `ilmExplain` was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'horribly wrong', + ilmExplain: mockIlmExplain, + patternRollup: undefined, + stats: null, + }) + ).toBe(true); + }); + + test('it returns true if an error occurred, and all data was loaded', () => { + expect( + shouldCreatePatternRollup({ + error: 'over here', + ilmExplain: mockIlmExplain, + patternRollup: undefined, + stats: mockStats, + }) + ).toBe(true); + }); + }); + + describe('getIndexPropertiesContainerId', () => { + const pattern = 'auditbeat-*'; + + test('it returns the expected id', () => { + expect(getIndexPropertiesContainerId({ indexName, pattern })).toEqual( + 'index-properties-container-auditbeat-*.ds-packetbeat-8.6.1-2023.02.04-000001' + ); + }); + }); + + describe('getPageIndex', () => { + const getPageIndexArgs: { + indexName: string; + items: IndexSummaryTableItem[]; + pageSize: number; + } = { + indexName: 'auditbeat-7.17.9-2023.04.09-000001', // <-- on page 2 of 3 (page index 1) + items: [ + { + docsCount: 48077, + incompatible: undefined, + indexName: 'auditbeat-7.14.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43357342, + }, + { + docsCount: 48068, + incompatible: undefined, + indexName: 'auditbeat-7.3.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 32460397, + }, + { + docsCount: 48064, + incompatible: undefined, + indexName: 'auditbeat-7.11.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42782794, + }, + { + docsCount: 47868, + incompatible: undefined, + indexName: 'auditbeat-7.6.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 31575964, + }, + { + docsCount: 47827, + incompatible: 20, + indexName: 'auditbeat-7.15.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 44130657, + }, + { + docsCount: 47642, + incompatible: undefined, + indexName: '.ds-auditbeat-8.4.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42412521, + }, + { + docsCount: 47545, + incompatible: undefined, + indexName: 'auditbeat-7.16.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41423244, + }, + { + docsCount: 47531, + incompatible: undefined, + indexName: 'auditbeat-7.5.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 32394133, + }, + { + docsCount: 47530, + incompatible: undefined, + indexName: 'auditbeat-7.12.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43015519, + }, + { + docsCount: 47520, + incompatible: undefined, + indexName: '.ds-auditbeat-8.0.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42230604, + }, + { + docsCount: 47496, + incompatible: undefined, + indexName: '.ds-auditbeat-8.2.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41710968, + }, + { + docsCount: 47486, + incompatible: undefined, + indexName: '.ds-auditbeat-8.5.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42295944, + }, + { + docsCount: 47486, + incompatible: undefined, + indexName: '.ds-auditbeat-8.3.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41761321, + }, + { + docsCount: 47460, + incompatible: undefined, + indexName: 'auditbeat-7.2.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 30481198, + }, + { + docsCount: 47439, + incompatible: undefined, + indexName: 'auditbeat-7.17.9-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41554041, + }, + { + docsCount: 47395, + incompatible: undefined, + indexName: 'auditbeat-7.9.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 42815907, + }, + { + docsCount: 47394, + incompatible: undefined, + indexName: '.ds-auditbeat-8.7.0-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41157112, + }, + { + docsCount: 47372, + incompatible: undefined, + indexName: 'auditbeat-7.4.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 31626792, + }, + { + docsCount: 47369, + incompatible: undefined, + indexName: 'auditbeat-7.13.4-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41828969, + }, + { + docsCount: 47348, + incompatible: undefined, + indexName: 'auditbeat-7.7.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 40010773, + }, + { + docsCount: 47339, + incompatible: undefined, + indexName: 'auditbeat-7.10.2-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43480570, + }, + { + docsCount: 47325, + incompatible: undefined, + indexName: '.ds-auditbeat-8.1.3-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 41822475, + }, + { + docsCount: 47294, + incompatible: undefined, + indexName: 'auditbeat-7.8.0-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 43018490, + }, + { + docsCount: 24276, + incompatible: undefined, + indexName: '.ds-auditbeat-8.6.1-2023.04.09-000001', + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 23579440, + }, + { + docsCount: 4, + incompatible: undefined, + indexName: 'auditbeat-custom-index-1', + ilmPhase: 'unmanaged', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 28409, + }, + { + docsCount: 0, + incompatible: undefined, + indexName: 'auditbeat-custom-empty-index-1', + ilmPhase: 'unmanaged', + pattern: 'auditbeat-*', + patternDocsCount: 1118155, + sizeInBytes: 247, + }, + ], + pageSize: 10, + }; + + test('it returns the expected page index', () => { + expect(getPageIndex(getPageIndexArgs)).toEqual(1); + }); + + test('it returns the expected page index for the first item', () => { + const firstItemIndexName = 'auditbeat-7.14.2-2023.04.09-000001'; + + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: firstItemIndexName, + }) + ).toEqual(0); + }); + + test('it returns the expected page index for the last item', () => { + const lastItemIndexName = 'auditbeat-custom-empty-index-1'; + + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: lastItemIndexName, + }) + ).toEqual(2); + }); + + test('it returns null when the index cannot be found', () => { + expect( + getPageIndex({ + ...getPageIndexArgs, + indexName: 'does_not_exist', // <-- this index is not in the items + }) + ).toBeNull(); + }); + + test('it returns null when `pageSize` is zero', () => { + expect( + getPageIndex({ + ...getPageIndexArgs, + pageSize: 0, // <-- invalid + }) + ).toBeNull(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts index efa9dbfa69d5e..40d0bbfb26293 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/helpers.ts @@ -9,7 +9,7 @@ import type { IlmExplainLifecycleLifecycleExplain, IndicesStatsIndicesStats, } from '@elastic/elasticsearch/lib/api/types'; -import { sortBy } from 'lodash/fp'; +import { orderBy } from 'lodash/fp'; import type { IndexSummaryTableItem } from '../summary_table/helpers'; import type { @@ -17,8 +17,9 @@ import type { IlmExplainPhaseCounts, DataQualityCheckResult, PatternRollup, + SortConfig, } from '../../types'; -import { getDocsCount } from '../../helpers'; +import { getDocsCount, getSizeInBytes } from '../../helpers'; export const isManaged = ( ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined @@ -144,6 +145,8 @@ export const getSummaryTableItems = ({ pattern, patternDocsCount, results, + sortByColumn, + sortByDirection, stats, }: { ilmExplain: Record | null; @@ -151,6 +154,8 @@ export const getSummaryTableItems = ({ pattern: string; patternDocsCount: number; results: Record | undefined; + sortByColumn: string; + sortByDirection: 'desc' | 'asc'; stats: Record | null; }): IndexSummaryTableItem[] => { const summaryTableItems = indexNames.map((indexName) => ({ @@ -160,47 +165,10 @@ export const getSummaryTableItems = ({ ilmPhase: ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined, pattern, patternDocsCount, + sizeInBytes: getSizeInBytes({ stats, indexName }), })); - return sortBy('docsCount', summaryTableItems).reverse(); -}; - -export const getDefaultIndexIncompatibleCounts = ( - indexNames: string[] -): Record => - indexNames.reduce>( - (acc, indexName) => ({ - ...acc, - [indexName]: undefined, - }), - {} - ); - -export const createPatternIncompatibleEntries = ({ - indexNames, - patternIncompatible, -}: { - indexNames: string[]; - patternIncompatible: Record; -}): Record => - indexNames.reduce>( - (acc, indexName) => - indexName in patternIncompatible - ? { ...acc, [indexName]: patternIncompatible[indexName] } - : { ...acc, [indexName]: undefined }, - {} - ); - -export const getIncompatible = ( - patternIncompatible: Record -): number | undefined => { - const allIndexes = Object.values(patternIncompatible); - const allIndexesHaveValues = allIndexes.every((incompatible) => Number.isInteger(incompatible)); - - // only return a number when all indexes have an `incompatible` count: - return allIndexesHaveValues - ? allIndexes.reduce((acc, incompatible) => acc + Number(incompatible), 0) - : undefined; + return orderBy([sortByColumn], [sortByDirection], summaryTableItems); }; export const shouldCreateIndexNames = ({ @@ -233,3 +201,38 @@ export const shouldCreatePatternRollup = ({ return allDataLoaded || errorOccurred; }; + +export const getIndexPropertiesContainerId = ({ + indexName, + pattern, +}: { + indexName: string; + pattern: string; +}): string => `index-properties-container-${pattern}${indexName}`; + +export const defaultSort: SortConfig = { + sort: { + direction: 'desc', + field: 'docsCount', + }, +}; + +export const MIN_PAGE_SIZE = 10; + +export const getPageIndex = ({ + indexName, + items, + pageSize, +}: { + indexName: string; + items: IndexSummaryTableItem[]; + pageSize: number; +}): number | null => { + const index = items.findIndex((x) => x.indexName === indexName); + + if (index !== -1 && pageSize !== 0) { + return Math.floor(index / pageSize); + } else { + return null; + } +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx index 4fa3b34884929..d9b002a63dc68 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.test.tsx @@ -6,12 +6,22 @@ */ import { DARK_THEME } from '@elastic/charts'; +import numeral from '@elastic/numeral'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from '../../mock/test_providers'; +import { EMPTY_STAT } from '../../helpers'; +import { TestProviders } from '../../mock/test_providers/test_providers'; import { Pattern } from '.'; +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + jest.mock('../../use_stats', () => ({ useStats: jest.fn(() => ({ stats: {}, @@ -31,12 +41,15 @@ jest.mock('../../use_ilm_explain', () => ({ const defaultProps = { addSuccessToast: jest.fn(), canUserCreateAndReadCases: jest.fn(), - defaultNumberFormat: '0,0.[000]', + formatBytes, + formatNumber, getGroupByFieldsOnClick: jest.fn(), ilmPhases: ['hot', 'warm', 'unmanaged'], indexNames: undefined, openCreateCaseFlyout: jest.fn(), patternRollup: undefined, + selectedIndex: null, + setSelectedIndex: jest.fn(), theme: DARK_THEME, updatePatternIndexNames: jest.fn(), updatePatternRollup: jest.fn(), diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx index bd6479490dab8..cb430d75ef12e 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/index.tsx @@ -16,14 +16,17 @@ import type { } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import { ErrorEmptyPrompt } from '../error_empty_prompt'; import { + defaultSort, getIlmExplainPhaseCounts, getIlmPhase, + getPageIndex, getSummaryTableItems, + MIN_PAGE_SIZE, shouldCreateIndexNames, shouldCreatePatternRollup, } from './helpers'; @@ -33,6 +36,7 @@ import { getTotalDocsCount, getTotalPatternIncompatible, getTotalPatternIndicesChecked, + getTotalSizeInBytes, } from '../../helpers'; import { IndexProperties } from '../index_properties'; import { LoadingEmptyPrompt } from '../loading_empty_prompt'; @@ -41,7 +45,7 @@ import { RemoteClustersCallout } from '../remote_clusters_callout'; import { SummaryTable } from '../summary_table'; import { getSummaryTableColumns } from '../summary_table/helpers'; import * as i18n from './translations'; -import type { PatternRollup } from '../../types'; +import type { PatternRollup, SelectedIndex, SortConfig } from '../../types'; import { useIlmExplain } from '../../use_ilm_explain'; import { useStats } from '../../use_stats'; @@ -55,7 +59,8 @@ const EMPTY_INDEX_NAMES: string[] = []; interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; getGroupByFieldsOnClick: ( elements: Array< | FlameElementEvent @@ -80,6 +85,8 @@ interface Props { }) => void; pattern: string; patternRollup: PatternRollup | undefined; + selectedIndex: SelectedIndex | null; + setSelectedIndex: (selectedIndex: SelectedIndex | null) => void; theme: Theme; updatePatternIndexNames: ({ indexNames, @@ -94,17 +101,25 @@ interface Props { const PatternComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, getGroupByFieldsOnClick, indexNames, ilmPhases, openCreateCaseFlyout, pattern, patternRollup, + selectedIndex, + setSelectedIndex, theme, updatePatternIndexNames, updatePatternRollup, }) => { + const containerRef = useRef(null); + const [sorting, setSorting] = useState(defaultSort); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(MIN_PAGE_SIZE); + const { error: statsError, loading: loadingStats, stats } = useStats(pattern); const { error: ilmExplainError, loading: loadingIlmExplain, ilmExplain } = useIlmExplain(pattern); @@ -129,7 +144,8 @@ const PatternComponent: React.FC = ({ = ({ [ addSuccessToast, canUserCreateAndReadCases, - defaultNumberFormat, + formatBytes, + formatNumber, getGroupByFieldsOnClick, ilmExplain, itemIdToExpandedRowMap, @@ -171,9 +188,20 @@ const PatternComponent: React.FC = ({ pattern, patternDocsCount: patternRollup?.docsCount ?? 0, results: patternRollup?.results, + sortByColumn: sorting.sort.field, + sortByDirection: sorting.sort.direction, stats, }), - [ilmExplain, indexNames, pattern, patternRollup, stats] + [ + ilmExplain, + indexNames, + pattern, + patternRollup?.docsCount, + patternRollup?.results, + sorting.sort.direction, + sorting.sort.field, + stats, + ] ); useEffect(() => { @@ -196,6 +224,10 @@ const PatternComponent: React.FC = ({ indices: getIndexNames({ stats, ilmExplain, ilmPhases }).length, pattern, results: undefined, + sizeInBytes: getTotalSizeInBytes({ + indexNames: getIndexNames({ stats, ilmExplain, ilmPhases }), + stats, + }), stats, }); } @@ -212,18 +244,49 @@ const PatternComponent: React.FC = ({ updatePatternRollup, ]); + useEffect(() => { + if (selectedIndex?.pattern === pattern) { + const selectedPageIndex = getPageIndex({ + indexName: selectedIndex.indexName, + items, + pageSize, + }); + + if (selectedPageIndex != null) { + setPageIndex(selectedPageIndex); + } + + if (itemIdToExpandedRowMap[selectedIndex.indexName] == null) { + toggleExpanded(selectedIndex.indexName); // expand the selected index + } + + containerRef.current?.scrollIntoView(); + setSelectedIndex(null); + } + }, [ + itemIdToExpandedRowMap, + items, + pageSize, + pattern, + selectedIndex, + setSelectedIndex, + toggleExpanded, + ]); + return ( - + @@ -242,19 +305,27 @@ const PatternComponent: React.FC = ({ {loading && } {!loading && error == null && ( - +
+ +
)}
); }; -PatternComponent.displayName = 'PatternComponent'; - export const Pattern = React.memo(PatternComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx index a8ae7a673b92e..233de9fc93a1f 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/index.tsx @@ -13,23 +13,27 @@ import { PatternLabel } from './pattern_label'; import { StatsRollup } from './stats_rollup'; interface Props { - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmExplainPhaseCounts: IlmExplainPhaseCounts; incompatible: number | undefined; indices: number | undefined; indicesChecked: number | undefined; pattern: string; patternDocsCount: number; + patternSizeInBytes: number; } const PatternSummaryComponent: React.FC = ({ - defaultNumberFormat, + formatBytes, + formatNumber, ilmExplainPhaseCounts, incompatible, indices, indicesChecked, pattern, patternDocsCount, + patternSizeInBytes, }) => ( @@ -44,12 +48,14 @@ const PatternSummaryComponent: React.FC = ({ diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.test.ts new file mode 100644 index 0000000000000..dfa285d60b40a --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/pattern_label/helpers.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { getResultToolTip, showResult } from './helpers'; +import { ALL_PASSED, SOME_FAILED, SOME_UNCHECKED } from './translations'; + +describe('helpers', () => { + describe('getResultToolTip', () => { + test('it returns the expected tool tip when `incompatible` is undefined', () => { + expect(getResultToolTip(undefined)).toEqual(SOME_UNCHECKED); + }); + + test('it returns the expected tool tip when `incompatible` is zero', () => { + expect(getResultToolTip(0)).toEqual(ALL_PASSED); + }); + + test('it returns the expected tool tip when `incompatible` is non-zero', () => { + expect(getResultToolTip(1)).toEqual(SOME_FAILED); + }); + }); + + describe('showResult', () => { + test('it returns true when `incompatible` is defined, and `indicesChecked` equals `indices`', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = 2; // all indices were checked + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(true); + }); + + test('it returns false when `incompatible` is defined, and `indices` does NOT equal `indicesChecked`', () => { + const incompatible = 0; // the one index checked (so far) didn't have any incompatible fields + const indicesChecked = 1; // only one index has been checked so far + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `incompatible` is undefined', () => { + const incompatible = undefined; // a state of undefined indicates there are no results + const indicesChecked = 1; // all indices were checked + const indices = 1; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `indices` is undefined', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = 2; // all indices were checked + const indices = undefined; // the total number of indices is unknown + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + + test('it returns false when `indicesChecked` is undefined', () => { + const incompatible = 0; // none of the indices checked had incompatible fields + const indicesChecked = undefined; // no indices were checked + const indices = 2; // total indices + + expect( + showResult({ + incompatible, + indices, + indicesChecked, + }) + ).toBe(false); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx index dcf28487a091c..4a51880404542 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/pattern_summary/stats_rollup/index.tsx @@ -6,7 +6,6 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useMemo } from 'react'; import styled from 'styled-components'; @@ -29,21 +28,25 @@ const DocsContainer = styled.div` const STAT_TITLE_SIZE = 's'; interface Props { - defaultNumberFormat: string; docsCount: number | undefined; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; incompatible: number | undefined; indices: number | undefined; indicesChecked: number | undefined; pattern?: string; + sizeInBytes: number | undefined; } const StatsRollupComponent: React.FC = ({ - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, incompatible, indices, indicesChecked, pattern, + sizeInBytes, }) => { const incompatibleDescription = useMemo( () => , @@ -53,11 +56,17 @@ const StatsRollupComponent: React.FC = ({ () => , [] ); + const sizeDescription = useMemo(() => , []); const docsDescription = useMemo(() => , []); const indicesDescription = useMemo(() => , []); return ( - + = ({ > @@ -88,11 +95,7 @@ const StatsRollupComponent: React.FC = ({ > @@ -110,7 +113,25 @@ const StatsRollupComponent: React.FC = ({ > + + + + + + + + @@ -126,9 +147,7 @@ const StatsRollupComponent: React.FC = ({ > diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/translations.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/translations.ts index 776ceef776172..79375b1956169 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/translations.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/pattern/translations.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; export const ERROR_LOADING_METADATA_TITLE = (pattern: string) => i18n.translate('ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingMetadataTitle', { values: { pattern }, - defaultMessage: "Indices matching the {pattern} pattern won't checked", + defaultMessage: "Indices matching the {pattern} pattern won't be checked", }); export const ERROR_LOADING_METADATA_BODY = ({ diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx index 94983d262404b..0c4a439f999bc 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/same_family/index.test.tsx @@ -9,7 +9,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { SAME_FAMILY } from './translations'; -import { TestProviders } from '../../mock/test_providers'; +import { TestProviders } from '../../mock/test_providers/test_providers'; import { SameFamily } from '.'; describe('SameFamily', () => { diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts index afaad5d08ed6e..1c52f29858ac4 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/stat_label/translations.ts @@ -69,6 +69,17 @@ export const INDICES = i18n.translate('ecsDataQualityDashboard.statLabels.indice defaultMessage: 'Indices', }); +export const SIZE = i18n.translate('ecsDataQualityDashboard.statLabels.sizeLabel', { + defaultMessage: 'Size', +}); + +export const INDICES_SIZE_PATTERN_TOOL_TIP = (pattern: string) => + i18n.translate('ecsDataQualityDashboard.statLabels.indicesSizePatternToolTip', { + values: { pattern }, + defaultMessage: + 'The total size of the primary indices matching the {pattern} pattern (does not include replicas)', + }); + export const TOTAL_COUNT_OF_INDICES_CHECKED_MATCHING_PATTERN_TOOL_TIP = (pattern: string) => i18n.translate( 'ecsDataQualityDashboard.statLabels.totalCountOfIndicesCheckedMatchingPatternToolTip', @@ -112,3 +123,10 @@ export const TOTAL_INDICES_TOOL_TIP = i18n.translate( defaultMessage: 'The total count of all indices', } ); + +export const TOTAL_SIZE_TOOL_TIP = i18n.translate( + 'ecsDataQualityDashboard.statLabels.totalSizeToolTip', + { + defaultMessage: 'The total size of all primary indices (does not include replicas)', + } +); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx new file mode 100644 index 0000000000000..f523c96cc0801 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { DARK_THEME, Settings } from '@elastic/charts'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { + FlattenedBucket, + getFlattenedBuckets, + getLegendItems, +} from '../body/data_quality_details/storage_details/helpers'; +import { EMPTY_STAT } from '../../helpers'; +import { alertIndexWithAllResults } from '../../mock/pattern_rollup/mock_alerts_pattern_rollup'; +import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { packetbeatNoResults } from '../../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import type { Props } from '.'; +import { StorageTreemap } from '.'; +import { DEFAULT_MAX_CHART_HEIGHT } from '../tabs/styles'; +import { NO_DATA_LABEL } from './translations'; +import { PatternRollup } from '../../types'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const ilmPhases = ['hot', 'warm', 'unmanaged']; +const patterns = ['.alerts-security.alerts-default', 'auditbeat-*', 'packetbeat-*']; + +const patternRollups: Record = { + '.alerts-security.alerts-default': alertIndexWithAllResults, + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, +}; + +const flattenedBuckets = getFlattenedBuckets({ + ilmPhases, + patternRollups, +}); + +const onIndexSelected = jest.fn(); + +const defaultProps: Props = { + flattenedBuckets, + formatBytes, + maxChartHeight: DEFAULT_MAX_CHART_HEIGHT, + onIndexSelected, + patternRollups, + patterns, + theme: DARK_THEME, +}; + +jest.mock('@elastic/charts', () => { + const actual = jest.requireActual('@elastic/charts'); + return { + ...actual, + Settings: jest.fn().mockReturnValue(null), + }; +}); + +describe('StorageTreemap', () => { + describe('when data is provided', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the treemap', () => { + expect(screen.getByTestId('storageTreemap').querySelector('.echChart')).toBeInTheDocument(); + }); + + test('it renders the legend with the expected overflow-y style', () => { + expect(screen.getByTestId('legend')).toHaveClass('eui-yScroll'); + }); + + test('it uses a theme with the expected `minFontSize` to show more labels at various screen resolutions', () => { + expect((Settings as jest.Mock).mock.calls[0][0].theme[0].partition.minFontSize).toEqual(4); + }); + + describe('legend items', () => { + const allLegendItems = getLegendItems({ patterns, flattenedBuckets, patternRollups }); + + describe('pattern legend items', () => { + const justPatterns = allLegendItems.filter((x) => x.ilmPhase == null); + + justPatterns.forEach(({ ilmPhase, index, pattern, sizeInBytes }) => { + test(`it renders the expend legend item for pattern: ilmPhase ${ilmPhase} pattern ${pattern} index ${index}`, () => { + expect( + screen.getByTestId(`chart-legend-item-${ilmPhase}${pattern}${index}`) + ).toHaveTextContent(`${pattern}${formatBytes(sizeInBytes)}`); + }); + }); + }); + + describe('index legend items', () => { + const justIndices = allLegendItems.filter((x) => x.ilmPhase != null); + + justIndices.forEach(({ ilmPhase, index, pattern, sizeInBytes }) => { + test(`it renders the expend legend item for index: ilmPhase ${ilmPhase} pattern ${pattern} index ${index}`, () => { + expect( + screen.getByTestId(`chart-legend-item-${ilmPhase}${pattern}${index}`) + ).toHaveTextContent(`${index}${formatBytes(sizeInBytes)}`); + }); + + test(`it invokes onIndexSelected() with the expected values for ilmPhase ${ilmPhase} pattern ${pattern} index ${index}`, () => { + const legendItem = screen.getByTestId( + `chart-legend-item-${ilmPhase}${pattern}${index}` + ); + + userEvent.click(legendItem); + + expect(onIndexSelected).toBeCalledWith({ indexName: index, pattern }); + }); + }); + }); + }); + }); + + describe('when the response does NOT have data', () => { + const emptyFlattenedBuckets: FlattenedBucket[] = []; + + beforeEach(() => { + render( + + + + ); + }); + + test('it does NOT render the treemap', () => { + expect(screen.queryByTestId('storageTreemap')).not.toBeInTheDocument(); + }); + + test('it does NOT render the legend', () => { + expect(screen.queryByTestId('legend')).not.toBeInTheDocument(); + }); + + test('it renders the "no data" message', () => { + expect(screen.getByText(NO_DATA_LABEL)).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx new file mode 100644 index 0000000000000..b42cd6072ad82 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/index.tsx @@ -0,0 +1,201 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import type { + Datum, + ElementClickListener, + FlameElementEvent, + HeatmapElementEvent, + MetricElementEvent, + PartialTheme, + PartitionElementEvent, + Theme, + WordCloudElementEvent, + XYChartElementEvent, +} from '@elastic/charts'; +import { Chart, Partition, PartitionLayout, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; + +import { + FlattenedBucket, + getLayersMultiDimensional, + getLegendItems, + getPathToFlattenedBucketMap, +} from '../body/data_quality_details/storage_details/helpers'; +import { ChartLegendItem } from '../../ecs_summary_donut_chart/chart_legend/chart_legend_item'; +import { NoData } from './no_data'; +import { ChartFlexItem, LegendContainer } from '../tabs/styles'; +import { PatternRollup, SelectedIndex } from '../../types'; + +export const DEFAULT_MIN_CHART_HEIGHT = 240; // px +export const LEGEND_WIDTH = 220; // px +export const LEGEND_TEXT_WITH = 120; // px + +export interface Props { + flattenedBuckets: FlattenedBucket[]; + formatBytes: (value: number | undefined) => string; + maxChartHeight?: number; + minChartHeight?: number; + onIndexSelected: ({ indexName, pattern }: SelectedIndex) => void; + patternRollups: Record; + patterns: string[]; + theme: Theme; +} + +interface GetGroupByFieldsResult { + pattern: string; + indexName: string; +} + +export const getGroupByFieldsOnClick = ( + elements: Array< + | FlameElementEvent + | HeatmapElementEvent + | MetricElementEvent + | PartitionElementEvent + | WordCloudElementEvent + | XYChartElementEvent + > +): GetGroupByFieldsResult => { + const flattened = elements.flat(2); + + const pattern = + flattened.length > 0 && 'groupByRollup' in flattened[0] && flattened[0].groupByRollup != null + ? `${flattened[0].groupByRollup}` + : ''; + + const indexName = + flattened.length > 1 && 'groupByRollup' in flattened[1] && flattened[1].groupByRollup != null + ? `${flattened[1].groupByRollup}` + : ''; + + return { + pattern, + indexName, + }; +}; + +const StorageTreemapComponent: React.FC = ({ + flattenedBuckets, + formatBytes, + maxChartHeight, + minChartHeight = DEFAULT_MIN_CHART_HEIGHT, + onIndexSelected, + patternRollups, + patterns, + theme, +}: Props) => { + const fillColor = useMemo(() => theme.background.color, [theme.background.color]); + + const treemapTheme: PartialTheme[] = useMemo( + () => [ + { + partition: { + fillLabel: { valueFont: { fontWeight: 700 } }, + idealFontSizeJump: 1.15, + maxFontSize: 16, + minFontSize: 4, + sectorLineStroke: fillColor, // draws the light or dark "lines" between partitions + sectorLineWidth: 1.5, + }, + }, + ], + [fillColor] + ); + + const onElementClick: ElementClickListener = useCallback( + (event) => { + const { indexName, pattern } = getGroupByFieldsOnClick(event); + + if (!isEmpty(indexName) && !isEmpty(pattern)) { + onIndexSelected({ indexName, pattern }); + } + }, + [onIndexSelected] + ); + + const pathToFlattenedBucketMap = getPathToFlattenedBucketMap(flattenedBuckets); + + const layers = useMemo( + () => + getLayersMultiDimensional({ + formatBytes, + layer0FillColor: fillColor, + pathToFlattenedBucketMap, + }), + [fillColor, formatBytes, pathToFlattenedBucketMap] + ); + + const valueAccessor = useCallback(({ sizeInBytes }: Datum) => sizeInBytes, []); + + const legendItems = useMemo( + () => getLegendItems({ patterns, flattenedBuckets, patternRollups }), + [flattenedBuckets, patternRollups, patterns] + ); + + if (flattenedBuckets.length === 0) { + return ; + } + + return ( + + + {flattenedBuckets.length === 0 ? ( + + ) : ( + + + formatBytes(d)} + /> + + )} + + + + + {legendItems.map(({ color, ilmPhase, index, pattern, sizeInBytes }) => ( + { + onIndexSelected({ indexName: index, pattern }); + } + : undefined + } + text={index ?? pattern} + textWidth={LEGEND_TEXT_WITH} + /> + ))} + + + + ); +}; + +export const StorageTreemap = React.memo(StorageTreemapComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx new file mode 100644 index 0000000000000..0cf39beae7b2d --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import * as i18n from '../translations'; + +import { NoData } from '.'; + +describe('NoData', () => { + test('renders the expected "no data" message', () => { + render(); + + expect(screen.getByText(i18n.NO_DATA_LABEL)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx new file mode 100644 index 0000000000000..a5edca17291d2 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/no_data/index.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +const NoDataLabel = styled(EuiText)` + text-align: center; +`; + +interface Props { + reason?: string; +} + +const NoDataComponent: React.FC = ({ reason }) => ( + + + + {i18n.NO_DATA_LABEL} + + + {reason != null && ( + <> + + + {reason} + + + )} + + +); + +NoDataComponent.displayName = 'NoDataComponent'; + +export const NoData = React.memo(NoDataComponent); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/translations.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/translations.ts new file mode 100644 index 0000000000000..f60cb2366cf36 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/storage_treemap/translations.ts @@ -0,0 +1,20 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const NO_DATA_LABEL = i18n.translate('ecsDataQualityDashboard.storageTreemap.noDataLabel', { + defaultMessage: 'No data to display', +}); + +export const NO_DATA_REASON_LABEL = (stackByField1: string) => + i18n.translate('ecsDataQualityDashboard.storageTreemap.noDataReasonLabel', { + values: { + stackByField1, + }, + defaultMessage: 'The {stackByField1} field was not present in any groups', + }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx new file mode 100644 index 0000000000000..e913d38dbb07c --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.test.tsx @@ -0,0 +1,543 @@ +/* + * 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 { EuiScreenReaderOnly, EuiTableFieldDataColumnType } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { omit } from 'lodash/fp'; +import React from 'react'; + +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { EMPTY_STAT } from '../../helpers'; +import { + getDocsCountPercent, + getResultIcon, + getResultIconColor, + getResultToolTip, + getShowPagination, + getSummaryTableColumns, + getToggleButtonId, + IndexSummaryTableItem, +} from './helpers'; +import { COLLAPSE, EXPAND, FAILED, PASSED, THIS_INDEX_HAS_NOT_BEEN_CHECKED } from './translations'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +describe('helpers', () => { + describe('getResultToolTip', () => { + test('it shows a "this index has not been checked" tool tip when `incompatible` is undefined', () => { + expect(getResultToolTip(undefined)).toEqual(THIS_INDEX_HAS_NOT_BEEN_CHECKED); + }); + + test('it returns Passed when `incompatible` is zero', () => { + expect(getResultToolTip(0)).toEqual(PASSED); + }); + + test('it returns Failed when `incompatible` is NOT zero', () => { + expect(getResultToolTip(1)).toEqual(FAILED); + }); + }); + + describe('getResultIconColor', () => { + test('it returns `ghost` when `incompatible` is undefined', () => { + expect(getResultIconColor(undefined)).toEqual('ghost'); + }); + + test('it returns `success` when `incompatible` is zero', () => { + expect(getResultIconColor(0)).toEqual('success'); + }); + + test('it returns `danger` when `incompatible` is NOT zero', () => { + expect(getResultIconColor(1)).toEqual('danger'); + }); + }); + + describe('getResultIcon', () => { + test('it returns `cross` when `incompatible` is undefined', () => { + expect(getResultIcon(undefined)).toEqual('cross'); + }); + + test('it returns `check` when `incompatible` is zero', () => { + expect(getResultIcon(0)).toEqual('check'); + }); + + test('it returns `cross` when `incompatible` is NOT zero', () => { + expect(getResultIcon(1)).toEqual('cross'); + }); + }); + + describe('getDocsCountPercent', () => { + test('it returns an empty string when `patternDocsCount` is zero', () => { + expect( + getDocsCountPercent({ + docsCount: 0, + patternDocsCount: 0, + }) + ).toEqual(''); + }); + + test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is undefined', () => { + expect( + getDocsCountPercent({ + docsCount: 2904, + locales: undefined, + patternDocsCount: 57410, + }) + ).toEqual('5.1%'); + }); + + test('it returns the expected format when when `patternDocsCount` is non-zero, and `locales` is provided', () => { + expect( + getDocsCountPercent({ + docsCount: 2904, + locales: 'en-US', + patternDocsCount: 57410, + }) + ).toEqual('5.1%'); + }); + }); + + describe('getToggleButtonId', () => { + test('it returns the expected id when the button is expanded', () => { + expect( + getToggleButtonId({ + indexName: 'auditbeat-custom-index-1', + isExpanded: true, + pattern: 'auditbeat-*', + }) + ).toEqual('collapseauditbeat-custom-index-1auditbeat-*'); + }); + + test('it returns the expected id when the button is collapsed', () => { + expect( + getToggleButtonId({ + indexName: 'auditbeat-custom-index-1', + isExpanded: false, + pattern: 'auditbeat-*', + }) + ).toEqual('expandauditbeat-custom-index-1auditbeat-*'); + }); + }); + + describe('getSummaryTableColumns', () => { + const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001'; + + const indexSummaryTableItem: IndexSummaryTableItem = { + indexName, + docsCount: 2796, + incompatible: undefined, + ilmPhase: 'hot', + pattern: 'auditbeat-*', + patternDocsCount: 57410, + sizeInBytes: 103344068, + }; + + const hasIncompatible: IndexSummaryTableItem = { + ...indexSummaryTableItem, + incompatible: 1, // <-- one incompatible field + }; + + test('it returns the expected column configuration', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }).map((x) => omit('render', x)); + + expect(columns).toEqual([ + { + align: 'right', + isExpander: true, + name: ( + + {'Expand rows'} + + ), + width: '40px', + }, + { + field: 'incompatible', + name: 'Result', + sortable: true, + truncateText: false, + width: '50px', + }, + { field: 'indexName', name: 'Index', sortable: true, truncateText: false, width: '300px' }, + { field: 'docsCount', name: 'Docs', sortable: true, truncateText: false }, + { + field: 'incompatible', + name: 'Incompatible fields', + sortable: true, + truncateText: false, + }, + { field: 'ilmPhase', name: 'ILM Phase', sortable: true, truncateText: false }, + { field: 'sizeInBytes', name: 'Size', sortable: true, truncateText: false }, + ]); + }); + + describe('expand rows render()', () => { + test('it renders an Expand button when the row is NOT expanded', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) + .render; + + render( + + {expandRowsRender != null && + expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + expect(screen.getByLabelText(EXPAND)).toBeInTheDocument(); + }); + + test('it renders a Collapse button when the row is expanded', () => { + const itemIdToExpandedRowMap: Record = { + [indexName]: () => null, + }; + + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) + .render; + + render( + + {expandRowsRender != null && + expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + expect(screen.getByLabelText(COLLAPSE)).toBeInTheDocument(); + }); + + test('it invokes the `toggleExpanded` with the index name when the button is clicked', () => { + const toggleExpanded = jest.fn(); + + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded, + }); + const expandRowsRender = (columns[0] as EuiTableFieldDataColumnType) + .render; + + render( + + {expandRowsRender != null && + expandRowsRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + const button = screen.getByLabelText(EXPAND); + userEvent.click(button); + + expect(toggleExpanded).toBeCalledWith(indexName); + }); + }); + + describe('incompatible render()', () => { + test('it renders a placeholder when incompatible is undefined', () => { + const incompatibleIsUndefined: IndexSummaryTableItem = { + ...indexSummaryTableItem, + incompatible: undefined, // <-- + }; + + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const incompatibleRender = ( + columns[1] as EuiTableFieldDataColumnType + ).render; + + render( + + {incompatibleRender != null && + incompatibleRender(incompatibleIsUndefined, incompatibleIsUndefined)} + + ); + + expect(screen.getByTestId('incompatiblePlaceholder')).toHaveTextContent(EMPTY_STAT); + }); + + test('it renders the expected icon when there are incompatible fields', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const incompatibleRender = ( + columns[1] as EuiTableFieldDataColumnType + ).render; + + render( + + {incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)} + + ); + + expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'cross'); + }); + + test('it renders the expected icon when there are zero fields', () => { + const zeroIncompatible: IndexSummaryTableItem = { + ...indexSummaryTableItem, + incompatible: 0, // <-- one incompatible field + }; + + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const incompatibleRender = ( + columns[1] as EuiTableFieldDataColumnType + ).render; + + render( + + {incompatibleRender != null && incompatibleRender(zeroIncompatible, zeroIncompatible)} + + ); + + expect(screen.getByTestId('resultIcon')).toHaveAttribute('data-euiicon-type', 'check'); + }); + }); + + describe('indexName render()', () => { + test('it renders the index name', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const indexNameRender = (columns[2] as EuiTableFieldDataColumnType) + .render; + + render( + + {indexNameRender != null && + indexNameRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + expect(screen.getByTestId('indexName')).toHaveTextContent(indexName); + }); + }); + + describe('docsCount render()', () => { + beforeEach(() => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const docsCountRender = (columns[3] as EuiTableFieldDataColumnType) + .render; + + render( + + {docsCountRender != null && docsCountRender(hasIncompatible, hasIncompatible)} + + ); + }); + + test('it renders the expected value', () => { + expect(screen.getByTestId('docsCount')).toHaveAttribute( + 'value', + String(hasIncompatible.docsCount) + ); + }); + + test('it renders the expected max (progress)', () => { + expect(screen.getByTestId('docsCount')).toHaveAttribute( + 'max', + String(hasIncompatible.patternDocsCount) + ); + }); + }); + + describe('incompatible column render()', () => { + test('it renders the expected value', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const incompatibleRender = ( + columns[4] as EuiTableFieldDataColumnType + ).render; + + render( + + {incompatibleRender != null && incompatibleRender(hasIncompatible, hasIncompatible)} + + ); + + expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('1'); + }); + + test('it renders the expected placeholder when incompatible is undefined', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const incompatibleRender = ( + columns[4] as EuiTableFieldDataColumnType + ).render; + + render( + + {incompatibleRender != null && + incompatibleRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + expect(screen.getByTestId('incompatibleStat')).toHaveTextContent('-- --'); // the euiScreenReaderOnly content renders an additional set of -- + }); + }); + + describe('ilmPhase column render()', () => { + test('it renders the expected ilmPhase badge content', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) + .render; + + render( + + {ilmPhaseRender != null && ilmPhaseRender(hasIncompatible, hasIncompatible)} + + ); + + expect(screen.getByTestId('ilmPhase')).toHaveTextContent('hot'); + }); + + test('it does NOT render the ilmPhase badge when `ilmPhase` is undefined', () => { + const ilmPhaseIsUndefined: IndexSummaryTableItem = { + ...indexSummaryTableItem, + ilmPhase: undefined, // <-- + }; + + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType) + .render; + + render( + + {ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)} + + ); + + expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument(); + }); + }); + + describe('sizeInBytes render()', () => { + test('it renders the expected formatted bytes', () => { + const columns = getSummaryTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap: {}, + pattern: 'auditbeat-*', + toggleExpanded: jest.fn(), + }); + + const sizeInBytesRender = (columns[6] as EuiTableFieldDataColumnType) + .render; + + render( + + {sizeInBytesRender != null && + sizeInBytesRender(indexSummaryTableItem, indexSummaryTableItem)} + + ); + + expect(screen.getByTestId('sizeInBytes')).toHaveTextContent('98.6MB'); + }); + }); + }); + + describe('getShowPagination', () => { + test('it returns true when `totalItemCount` is greater than `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 11, + }) + ).toBe(true); + }); + + test('it returns false when `totalItemCount` equals `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 10, + }) + ).toBe(false); + }); + + test('it returns false when `totalItemCount` is less than `minPageSize`', () => { + expect( + getShowPagination({ + minPageSize: 10, + totalItemCount: 9, + }) + ).toBe(false); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx index e3789fce0bb5b..89e0d78fddb6b 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/helpers.tsx @@ -21,6 +21,7 @@ import styled from 'styled-components'; import { EMPTY_STAT, getIlmPhaseDescription, getIncompatibleStatColor } from '../../helpers'; import { INCOMPATIBLE_INDEX_TOOL_TIP } from '../stat_label/translations'; +import { INDEX_SIZE_TOOLTIP } from '../../translations'; import * as i18n from './translations'; import type { IlmPhase } from '../../types'; @@ -39,6 +40,7 @@ export interface IndexSummaryTableItem { ilmPhase: IlmPhase | undefined; pattern: string; patternDocsCount: number; + sizeInBytes: number; } export const getResultToolTip = (incompatible: number | undefined): string => { @@ -83,13 +85,27 @@ export const getDocsCountPercent = ({ }) : ''; +export const getToggleButtonId = ({ + indexName, + isExpanded, + pattern, +}: { + indexName: string; + isExpanded: boolean; + pattern: string; +}): string => (isExpanded ? `collapse${indexName}${pattern}` : `expand${indexName}${pattern}`); + export const getSummaryTableColumns = ({ + formatBytes, formatNumber, itemIdToExpandedRowMap, + pattern, toggleExpanded, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; itemIdToExpandedRowMap: Record; + pattern: string; toggleExpanded: (indexName: string) => void; }): Array> => [ { @@ -103,6 +119,11 @@ export const getSummaryTableColumns = ({ render: ({ indexName }: IndexSummaryTableItem) => ( toggleExpanded(indexName)} iconType={itemIdToExpandedRowMap[indexName] ? 'arrowDown' : 'arrowRight'} /> @@ -115,11 +136,15 @@ export const getSummaryTableColumns = ({ render: (_, { incompatible }) => incompatible != null ? ( - + ) : ( - {EMPTY_STAT} + {EMPTY_STAT} ), sortable: true, @@ -129,9 +154,11 @@ export const getSummaryTableColumns = ({ { field: 'indexName', name: i18n.INDEX, - render: (_, { indexName, pattern }) => ( + render: (_, { indexName }) => ( - {indexName} + + {indexName} + ), sortable: true, @@ -144,6 +171,7 @@ export const getSummaryTableColumns = ({ render: (_, { docsCount, patternDocsCount }) => ( ( ), - sortable: false, + sortable: true, truncateText: false, }, { @@ -178,10 +207,23 @@ export const getSummaryTableColumns = ({ render: (_, { ilmPhase }) => ilmPhase != null ? ( - {ilmPhase} + + {ilmPhase} + ) : null, - sortable: false, + sortable: true, + truncateText: false, + }, + { + field: 'sizeInBytes', + name: i18n.SIZE, + render: (_, { sizeInBytes }) => ( + + {formatBytes(sizeInBytes)} + + ), + sortable: true, truncateText: false, }, ]; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx new file mode 100644 index 0000000000000..235ec61a204af --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { EMPTY_STAT } from '../../helpers'; +import { getSummaryTableColumns } from './helpers'; +import { mockIlmExplain } from '../../mock/ilm_explain/mock_ilm_explain'; +import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { mockStats } from '../../mock/stats/mock_stats'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { getSummaryTableItems } from '../pattern/helpers'; +import { SortConfig } from '../../types'; +import { Props, SummaryTable } from '.'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const indexNames = [ + '.ds-auditbeat-8.6.1-2023.02.07-000001', + 'auditbeat-custom-empty-index-1', + 'auditbeat-custom-index-1', + '.internal.alerts-security.alerts-default-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + '.ds-packetbeat-8.6.1-2023.02.04-000001', +]; + +export const defaultSort: SortConfig = { + sort: { + direction: 'desc', + field: 'docsCount', + }, +}; + +const pattern = 'auditbeat-*'; + +const items = getSummaryTableItems({ + ilmExplain: mockIlmExplain, + indexNames: indexNames ?? [], + pattern, + patternDocsCount: auditbeatWithAllResults?.docsCount ?? 0, + results: auditbeatWithAllResults?.results, + sortByColumn: defaultSort.sort.field, + sortByDirection: defaultSort.sort.direction, + stats: mockStats, +}); + +const defaultProps: Props = { + formatBytes, + formatNumber, + getTableColumns: getSummaryTableColumns, + itemIdToExpandedRowMap: {}, + items, + pageIndex: 0, + pageSize: 10, + pattern, + setPageIndex: jest.fn(), + setPageSize: jest.fn(), + setSorting: jest.fn(), + sorting: defaultSort, + toggleExpanded: jest.fn(), +}; + +describe('SummaryTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + + render( + + + + ); + }); + + test('it renders the summary table', () => { + expect(screen.getByTestId('summaryTable')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx index 7b02add151803..2dd2c4e214dc0 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/summary_table/index.tsx @@ -5,79 +5,79 @@ * 2.0. */ -import type { - CriteriaWithPagination, - Direction, - EuiBasicTableColumn, - Pagination, -} from '@elastic/eui'; +import type { CriteriaWithPagination, EuiBasicTableColumn, Pagination } from '@elastic/eui'; import { EuiInMemoryTable } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; -import { EMPTY_STAT } from '../../helpers'; import type { IndexSummaryTableItem } from './helpers'; import { getShowPagination } from './helpers'; +import { defaultSort, MIN_PAGE_SIZE } from '../pattern/helpers'; +import { SortConfig } from '../../types'; -const MIN_PAGE_SIZE = 10; - -interface SortConfig { - sort: { - direction: Direction; - field: string; - }; -} - -const defaultSort: SortConfig = { - sort: { - direction: 'desc', - field: 'docsCount', - }, -}; - -interface Props { - defaultNumberFormat: string; +export interface Props { + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; getTableColumns: ({ + formatBytes, formatNumber, itemIdToExpandedRowMap, + pattern, toggleExpanded, }: { + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; itemIdToExpandedRowMap: Record; + pattern: string; toggleExpanded: (indexName: string) => void; }) => Array>; itemIdToExpandedRowMap: Record; items: IndexSummaryTableItem[]; + pageIndex: number; + pageSize: number; + pattern: string; + setPageIndex: (pageIndex: number) => void; + setPageSize: (pageSize: number) => void; + setSorting: (sortConfig: SortConfig) => void; + sorting: SortConfig; toggleExpanded: (indexName: string) => void; } const SummaryTableComponent: React.FC = ({ - defaultNumberFormat, + formatBytes, + formatNumber, getTableColumns, itemIdToExpandedRowMap, items, + pageIndex, + pageSize, + pattern, + setPageIndex, + setPageSize, + setSorting, + sorting, toggleExpanded, }) => { - const [sorting, setSorting] = useState(defaultSort); - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(MIN_PAGE_SIZE); const columns = useMemo( - () => getTableColumns({ formatNumber, itemIdToExpandedRowMap, toggleExpanded }), - [formatNumber, getTableColumns, itemIdToExpandedRowMap, toggleExpanded] + () => + getTableColumns({ + formatBytes, + formatNumber, + itemIdToExpandedRowMap, + pattern, + toggleExpanded, + }), + [formatBytes, formatNumber, getTableColumns, itemIdToExpandedRowMap, pattern, toggleExpanded] ); const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []); - const onChange = useCallback(({ page, sort }: CriteriaWithPagination) => { - setSorting({ sort: sort ?? defaultSort.sort }); - - setPageIndex(page.index); - setPageSize(page.size); - }, []); + const onChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + setSorting({ sort: sort ?? defaultSort.sort }); + setPageIndex(page.index); + setPageSize(page.size); + }, + [setPageIndex, setPageSize, setSorting] + ); const pagination: Pagination = useMemo( () => ({ @@ -91,9 +91,10 @@ const SummaryTableComponent: React.FC = ({ return ( + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; describe('helpers', () => { describe('getCustomMarkdownComment', () => { @@ -23,4 +44,52 @@ ${ECS_IS_A_PERMISSIVE_SCHEMA} `); }); }); + + describe('showCustomCallout', () => { + test('it returns false when `enrichedFieldMetadata` is empty', () => { + expect(showCustomCallout([])).toBe(false); + }); + + test('it returns true when `enrichedFieldMetadata` is NOT empty', () => { + expect(showCustomCallout([someField])).toBe(true); + }); + }); + + describe('getCustomColor', () => { + test('it returns the expected color when there are custom fields', () => { + expect(getCustomColor(mockPartitionedFieldMetadata)).toEqual(euiThemeVars.euiColorLightShade); + }); + + test('it returns the expected color when custom fields is empty', () => { + const noCustomFields: PartitionedFieldMetadata = { + ...mockPartitionedFieldMetadata, + custom: [], // <-- empty + }; + + expect(getCustomColor(noCustomFields)).toEqual(euiThemeVars.euiTextColor); + }); + }); + + describe('getAllCustomMarkdownComments', () => { + test('it returns the expected comment', () => { + expect( + getAllCustomMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`, + '#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n', + ]); + }); + }); }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts index 1d8de8ac57593..7701db46d6c98 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/helpers.ts @@ -10,14 +10,11 @@ import { euiThemeVars } from '@kbn/ui-theme'; import { FIELD, INDEX_MAPPING_TYPE } from '../../../compare_fields_table/translations'; import { - ECS_FIELD_REFERENCE_URL, - ECS_REFERENCE_URL, getSummaryMarkdownComment, getCustomMarkdownTableRows, getMarkdownComment, getMarkdownTable, getTabCountsMarkdownComment, - MAPPING_URL, getSummaryTableMarkdownComment, } from '../../index_properties/markdown/helpers'; import * as i18n from '../../index_properties/translations'; @@ -50,33 +47,33 @@ export const getCustomColor = (partitionedFieldMetadata: PartitionedFieldMetadat export const getAllCustomMarkdownComments = ({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; + sizeInBytes: number | undefined; }): string[] => [ - getSummaryMarkdownComment({ - ecsFieldReferenceUrl: ECS_FIELD_REFERENCE_URL, - ecsReferenceUrl: ECS_REFERENCE_URL, - incompatible: partitionedFieldMetadata.incompatible.length, - indexName, - mappingUrl: MAPPING_URL, - }), + getSummaryMarkdownComment(indexName), getSummaryTableMarkdownComment({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }), getTabCountsMarkdownComment(partitionedFieldMetadata), getCustomMarkdownComment({ diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx index bfffbd1393ef6..09f7136d7108a 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/custom_tab/index.tsx @@ -13,13 +13,11 @@ import { EuiEmptyPrompt, EuiSpacer, } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useCallback, useMemo } from 'react'; import { CustomCallout } from '../callouts/custom_callout'; import { CompareFieldsTable } from '../../../compare_fields_table'; import { getCustomTableColumns } from '../../../compare_fields_table/helpers'; -import { EMPTY_STAT } from '../../../helpers'; import { EmptyPromptBody } from '../../index_properties/empty_prompt_body'; import { EmptyPromptTitle } from '../../index_properties/empty_prompt_title'; import { getAllCustomMarkdownComments, showCustomCallout } from './helpers'; @@ -29,39 +27,49 @@ import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; interface Props { addSuccessToast: (toast: { title: string }) => void; - defaultNumberFormat: string; docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; + sizeInBytes: number | undefined; } const CustomTabComponent: React.FC = ({ addSuccessToast, - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }) => { - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); const markdownComments: string[] = useMemo( () => getAllCustomMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }), - [docsCount, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount] + [ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + partitionedFieldMetadata, + patternDocsCount, + sizeInBytes, + ] ); const body = useMemo(() => , []); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx new file mode 100644 index 0000000000000..c0dc6a8aaafe2 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.test.tsx @@ -0,0 +1,112 @@ +/* + * 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 { DARK_THEME } from '@elastic/charts'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { omit } from 'lodash/fp'; + +import { + eventCategory, + someField, + timestamp, +} from '../../mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { mockPartitionedFieldMetadata } from '../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { mockStatsGreenIndex } from '../../mock/stats/mock_stats_green_index'; +import { + getEcsCompliantColor, + getMissingTimestampComment, + getTabs, + showMissingTimestampCallout, +} from './helpers'; + +describe('helpers', () => { + describe('getMissingTimestampComment', () => { + test('it returns the expected comment', () => { + expect(getMissingTimestampComment()).toEqual( + '#### Missing an @timestamp (date) field mapping for this index\n\nConsider adding an @timestamp (date) field mapping to this index, as required by the Elastic Common Schema (ECS), because:\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n' + ); + }); + }); + + describe('showMissingTimestampCallout', () => { + test('it returns true when `enrichedFieldMetadata` is empty', () => { + expect(showMissingTimestampCallout([])).toBe(true); + }); + + test('it returns false when `enrichedFieldMetadata` contains an @timestamp field', () => { + expect(showMissingTimestampCallout([timestamp, eventCategory, someField])).toBe(false); + }); + + test('it returns true when `enrichedFieldMetadata` does NOT contain an @timestamp field', () => { + expect(showMissingTimestampCallout([eventCategory, someField])).toBe(true); + }); + }); + + describe('getEcsCompliantColor', () => { + test('it returns the expected color for the ECS compliant data when the data includes an @timestamp', () => { + expect(getEcsCompliantColor(mockPartitionedFieldMetadata)).toEqual( + euiThemeVars.euiColorSuccess + ); + }); + + test('it returns the expected color for the ECS compliant data does NOT includes an @timestamp', () => { + const noTimestamp = { + ...mockPartitionedFieldMetadata, + ecsCompliant: mockPartitionedFieldMetadata.ecsCompliant.filter( + ({ name }) => name !== '@timestamp' + ), + }; + + expect(getEcsCompliantColor(noTimestamp)).toEqual(euiThemeVars.euiColorDanger); + }); + }); + + describe('getTabs', () => { + test('it returns the expected tabs', () => { + expect( + getTabs({ + addSuccessToast: jest.fn(), + addToNewCaseDisabled: false, + docsCount: 4, + formatBytes: jest.fn(), + formatNumber: jest.fn(), + getGroupByFieldsOnClick: jest.fn(), + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + onAddToNewCase: jest.fn(), + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'auditbeat-*', + patternDocsCount: 57410, + setSelectedTabId: jest.fn(), + stats: mockStatsGreenIndex, + theme: DARK_THEME, + }).map((x) => omit(['append', 'content'], x)) + ).toEqual([ + { + id: 'summaryTab', + name: 'Summary', + }, + { + id: 'incompatibleTab', + name: 'Incompatible fields', + }, + { + id: 'customTab', + name: 'Custom fields', + }, + { + id: 'ecsCompliantTab', + name: 'ECS compliant fields', + }, + { + id: 'allTab', + name: 'All fields', + }, + ]); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx index a98ad92ba6cd9..c0cbebd45cb8d 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/helpers.tsx @@ -14,6 +14,7 @@ import type { WordCloudElementEvent, XYChartElementEvent, } from '@elastic/charts'; +import type { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; import { EuiBadge } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import React from 'react'; @@ -35,6 +36,7 @@ import { getFillColor } from './summary_tab/helpers'; import * as i18n from '../index_properties/translations'; import { SummaryTab } from './summary_tab'; import type { EnrichedFieldMetadata, IlmPhase, PartitionedFieldMetadata } from '../../types'; +import { getSizeInBytes } from '../../helpers'; export const getMissingTimestampComment = (): string => getMarkdownComment({ @@ -48,7 +50,7 @@ ${i18n.PAGES_MAY_NOT_DISPLAY_EVENTS} export const showMissingTimestampCallout = ( enrichedFieldMetadata: EnrichedFieldMetadata[] -): boolean => enrichedFieldMetadata.length === 0; +): boolean => !enrichedFieldMetadata.some((x) => x.name === '@timestamp'); export const getEcsCompliantColor = (partitionedFieldMetadata: PartitionedFieldMetadata): string => showMissingTimestampCallout(partitionedFieldMetadata.ecsCompliant) @@ -58,8 +60,9 @@ export const getEcsCompliantColor = (partitionedFieldMetadata: PartitionedFieldM export const getTabs = ({ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, getGroupByFieldsOnClick, ilmPhase, indexName, @@ -68,11 +71,13 @@ export const getTabs = ({ pattern, patternDocsCount, setSelectedTabId, + stats, theme, }: { addSuccessToast: (toast: { title: string }) => void; addToNewCaseDisabled: boolean; - defaultNumberFormat: string; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; docsCount: number; getGroupByFieldsOnClick: ( elements: Array< @@ -94,6 +99,7 @@ export const getTabs = ({ pattern: string; patternDocsCount: number; setSelectedTabId: (tabId: string) => void; + stats: Record | null; theme: Theme; }) => [ { @@ -101,7 +107,8 @@ export const getTabs = ({ ), @@ -127,13 +135,15 @@ export const getTabs = ({ ), id: INCOMPATIBLE_TAB_ID, @@ -148,12 +158,14 @@ export const getTabs = ({ content: ( ), id: 'customTab', diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts index e82bfd4d2efdc..54babce560f25 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.test.ts @@ -5,11 +5,20 @@ * 2.0. */ -import { EcsVersion } from '@kbn/ecs'; import numeral from '@elastic/numeral'; +import { EcsVersion } from '@kbn/ecs'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { + getAllIncompatibleMarkdownComments, + getIncompatibleColor, + getIncompatibleFieldsMarkdownComment, + getIncompatibleFieldsMarkdownTablesComment, + getIncompatibleMappings, + getIncompatibleValues, + showInvalidCallout, +} from './helpers'; import { EMPTY_STAT } from '../../../helpers'; -import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; import { DETECTION_ENGINE_RULES_MAY_NOT_MATCH, INCOMPATIBLE_FIELDS_WITH, @@ -17,10 +26,8 @@ import { PAGES_MAY_NOT_DISPLAY_EVENTS, WHEN_AN_INCOMPATIBLE_FIELD, } from '../../index_properties/translations'; -import { - getIncompatibleFieldsMarkdownComment, - getAllIncompatibleMarkdownComments, -} from './helpers'; +import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { PartitionedFieldMetadata } from '../../../types'; describe('helpers', () => { describe('getIncompatibleFieldsMarkdownComment', () => { @@ -44,27 +51,313 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS} }); }); + describe('showInvalidCallout', () => { + test('it returns false when the `enrichedFieldMetadata` is empty', () => { + expect(showInvalidCallout([])).toBe(false); + }); + + test('it returns true when the `enrichedFieldMetadata` is NOT empty', () => { + expect(showInvalidCallout(mockPartitionedFieldMetadata.incompatible)).toBe(true); + }); + }); + + describe('getIncompatibleColor', () => { + test('it returns the expected color', () => { + expect(getIncompatibleColor()).toEqual(euiThemeVars.euiColorDanger); + }); + }); + + describe('getIncompatibleMappings', () => { + test('it (only) returns the mappings where type !== indexFieldType', () => { + expect(getIncompatibleMappings(mockPartitionedFieldMetadata.incompatible)).toEqual([ + { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + hasEcsMetadata: true, + ignore_above: 1024, + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + }, + { + dashed_name: 'source-ip', + description: 'IP address of the source (IPv4 or IPv6).', + flat_name: 'source.ip', + hasEcsMetadata: true, + indexFieldName: 'source.ip', + indexFieldType: 'text', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + level: 'core', + name: 'ip', + normalize: [], + short: 'IP address of the source.', + type: 'ip', + }, + ]); + }); + + test('it filters-out ECS complaint fields', () => { + expect(getIncompatibleMappings(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); + }); + }); + + describe('getIncompatibleValues', () => { + test('it (only) returns the mappings with indexInvalidValues', () => { + expect(getIncompatibleValues(mockPartitionedFieldMetadata.incompatible)).toEqual([ + { + allowed_values: [ + { + description: + 'Events in this category are related to the challenge and response process in which credentials are supplied and verified to allow the creation of a session. Common sources for these logs are Windows event logs and ssh logs. Visualize and analyze events in this category to look for failed logins, and other authentication-related activity.', + expected_event_types: ['start', 'end', 'info'], + name: 'authentication', + }, + { + description: + 'Events in the configuration category have to deal with creating, modifying, or deleting the settings or parameters of an application, process, or system.\nExample sources include security policy change logs, configuration auditing logging, and system integrity monitoring.', + expected_event_types: ['access', 'change', 'creation', 'deletion', 'info'], + name: 'configuration', + }, + { + description: + 'The database category denotes events and metrics relating to a data storage and retrieval system. Note that use of this category is not limited to relational database systems. Examples include event logs from MS SQL, MySQL, Elasticsearch, MongoDB, etc. Use this category to visualize and analyze database activity such as accesses and changes.', + expected_event_types: ['access', 'change', 'info', 'error'], + name: 'database', + }, + { + description: + 'Events in the driver category have to do with operating system device drivers and similar software entities such as Windows drivers, kernel extensions, kernel modules, etc.\nUse events and metrics in this category to visualize and analyze driver-related activity and status on hosts.', + expected_event_types: ['change', 'end', 'info', 'start'], + name: 'driver', + }, + { + description: + 'This category is used for events relating to email messages, email attachments, and email network or protocol activity.\nEmails events can be produced by email security gateways, mail transfer agents, email cloud service providers, or mail server monitoring applications.', + expected_event_types: ['info'], + name: 'email', + }, + { + description: + 'Relating to a set of information that has been created on, or has existed on a filesystem. Use this category of events to visualize and analyze the creation, access, and deletions of files. Events in this category can come from both host-based and network-based sources. An example source of a network-based detection of a file transfer would be the Zeek file.log.', + expected_event_types: ['change', 'creation', 'deletion', 'info'], + name: 'file', + }, + { + description: + 'Use this category to visualize and analyze information such as host inventory or host lifecycle events.\nMost of the events in this category can usually be observed from the outside, such as from a hypervisor or a control plane\'s point of view. Some can also be seen from within, such as "start" or "end".\nNote that this category is for information about hosts themselves; it is not meant to capture activity "happening on a host".', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'host', + }, + { + description: + 'Identity and access management (IAM) events relating to users, groups, and administration. Use this category to visualize and analyze IAM-related logs and data from active directory, LDAP, Okta, Duo, and other IAM systems.', + expected_event_types: [ + 'admin', + 'change', + 'creation', + 'deletion', + 'group', + 'info', + 'user', + ], + name: 'iam', + }, + { + description: + 'Relating to intrusion detections from IDS/IPS systems and functions, both network and host-based. Use this category to visualize and analyze intrusion detection alerts from systems such as Snort, Suricata, and Palo Alto threat detections.', + expected_event_types: ['allowed', 'denied', 'info'], + name: 'intrusion_detection', + }, + { + description: + 'Malware detection events and alerts. Use this category to visualize and analyze malware detections from EDR/EPP systems such as Elastic Endpoint Security, Symantec Endpoint Protection, Crowdstrike, and network IDS/IPS systems such as Suricata, or other sources of malware-related events such as Palo Alto Networks threat logs and Wildfire logs.', + expected_event_types: ['info'], + name: 'malware', + }, + { + description: + 'Relating to all network activity, including network connection lifecycle, network traffic, and essentially any event that includes an IP address. Many events containing decoded network protocol transactions fit into this category. Use events in this category to visualize or analyze counts of network ports, protocols, addresses, geolocation information, etc.', + expected_event_types: [ + 'access', + 'allowed', + 'connection', + 'denied', + 'end', + 'info', + 'protocol', + 'start', + ], + name: 'network', + }, + { + description: + 'Relating to software packages installed on hosts. Use this category to visualize and analyze inventory of software installed on various hosts, or to determine host vulnerability in the absence of vulnerability scan data.', + expected_event_types: [ + 'access', + 'change', + 'deletion', + 'info', + 'installation', + 'start', + ], + name: 'package', + }, + { + description: + 'Use this category of events to visualize and analyze process-specific information such as lifecycle events or process ancestry.', + expected_event_types: ['access', 'change', 'end', 'info', 'start'], + name: 'process', + }, + { + description: + 'Having to do with settings and assets stored in the Windows registry. Use this category to visualize and analyze activity such as registry access and modifications.', + expected_event_types: ['access', 'change', 'creation', 'deletion'], + name: 'registry', + }, + { + description: + 'The session category is applied to events and metrics regarding logical persistent connections to hosts and services. Use this category to visualize and analyze interactive or automated persistent connections between assets. Data for this category may come from Windows Event logs, SSH logs, or stateless sessions such as HTTP cookie-based sessions, etc.', + expected_event_types: ['start', 'end', 'info'], + name: 'session', + }, + { + description: + "Use this category to visualize and analyze events describing threat actors' targets, motives, or behaviors.", + expected_event_types: ['indicator'], + name: 'threat', + }, + { + description: + 'Relating to vulnerability scan results. Use this category to analyze vulnerabilities detected by Tenable, Qualys, internal scanners, and other vulnerability management sources.', + expected_event_types: ['info'], + name: 'vulnerability', + }, + { + description: + 'Relating to web server access. Use this category to create a dashboard of web server/proxy activity from apache, IIS, nginx web servers, etc. Note: events from network observers such as Zeek http log may also be included in this category.', + expected_event_types: ['access', 'error', 'info'], + name: 'web', + }, + ], + dashed_name: 'event-category', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy.\n`event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory.\nThis field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + flat_name: 'event.category', + ignore_above: 1024, + level: 'core', + name: 'category', + normalize: ['array'], + short: 'Event category. The second categorization field in the hierarchy.', + type: 'keyword', + indexFieldName: 'event.category', + indexFieldType: 'keyword', + indexInvalidValues: [ + { count: 2, fieldName: 'an_invalid_category' }, + { count: 1, fieldName: 'theory' }, + ], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: true, + }, + ]); + }); + + test('it filters-out ECS complaint fields', () => { + expect(getIncompatibleValues(mockPartitionedFieldMetadata.ecsCompliant)).toEqual([]); + }); + }); + + describe('getIncompatibleFieldsMarkdownTablesComment', () => { + test('it returns the expected comment when the index has `incompatibleMappings` and `incompatibleValues`', () => { + expect( + getIncompatibleFieldsMarkdownTablesComment({ + incompatibleMappings: [ + mockPartitionedFieldMetadata.incompatible[1], + mockPartitionedFieldMetadata.incompatible[2], + ], + incompatibleValues: [mockPartitionedFieldMetadata.incompatible[0]], + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual( + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n' + ); + }); + + test('it returns the expected comment when the index does NOT have `incompatibleMappings` and `incompatibleValues`', () => { + expect( + getIncompatibleFieldsMarkdownTablesComment({ + incompatibleMappings: [], // <-- no `incompatibleMappings` + incompatibleValues: [], // <-- no `incompatibleValues` + indexName: 'auditbeat-custom-index-1', + }) + ).toEqual('\n\n\n'); + }); + }); + describe('getAllIncompatibleMarkdownComments', () => { - test('it returns the expected collection of comments', () => { - const defaultNumberFormat = '0,0.[000]'; - const formatNumber = (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + const defaultBytesFormat = '0,0.[0]b'; + const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + + const defaultNumberFormat = '0,0.[000]'; + const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + test('it returns the expected collection of comments', () => { expect( getAllIncompatibleMarkdownComments({ docsCount: 4, + formatBytes, formatNumber, ilmPhase: 'unmanaged', indexName: 'auditbeat-custom-index-1', partitionedFieldMetadata: mockPartitionedFieldMetadata, patternDocsCount: 57410, + sizeInBytes: 28413, }) ).toEqual([ '### auditbeat-custom-index-1\n', - '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` |\n\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', `#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\n${INCOMPATIBLE_FIELDS_WITH}\n\n${WHEN_AN_INCOMPATIBLE_FIELD}\n${DETECTION_ENGINE_RULES_MAY_NOT_MATCH}\n${PAGES_MAY_NOT_DISPLAY_EVENTS}\n${MAPPINGS_THAT_CONFLICT_WITH_ECS}\n`, - '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2),\n`theory` (1) |\n\n', + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ]); + }); + + test('it returns the expected comment when `incompatible` is empty', () => { + const emptyIncompatible: PartitionedFieldMetadata = { + ...mockPartitionedFieldMetadata, + incompatible: [], // <-- empty + }; + + expect( + getAllIncompatibleMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + partitionedFieldMetadata: emptyIncompatible, + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + '\n\n\n', ]); }); }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts index 2e44ec53142b5..1f4e0b62b1c58 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/helpers.ts @@ -9,8 +9,6 @@ import { EcsVersion } from '@kbn/ecs'; import { getIncompatiableFieldsInSameFamilyCount } from '../callouts/incompatible_callout/helpers'; import { - ECS_FIELD_REFERENCE_URL, - ECS_REFERENCE_URL, getSummaryMarkdownComment, getIncompatibleMappingsMarkdownTableRows, getIncompatibleValuesMarkdownTableRows, @@ -18,7 +16,6 @@ import { getMarkdownTable, getSummaryTableMarkdownComment, getTabCountsMarkdownComment, - MAPPING_URL, } from '../../index_properties/markdown/helpers'; import { getFillColor } from '../summary_tab/helpers'; import * as i18n from '../../index_properties/translations'; @@ -106,18 +103,22 @@ ${ export const getAllIncompatibleMarkdownComments = ({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; + sizeInBytes: number | undefined; }): string[] => { const incompatibleMappings = getIncompatibleMappings(partitionedFieldMetadata.incompatible); const incompatibleValues = getIncompatibleValues(partitionedFieldMetadata.incompatible); @@ -134,20 +135,16 @@ export const getAllIncompatibleMarkdownComments = ({ : ''; return [ - getSummaryMarkdownComment({ - ecsFieldReferenceUrl: ECS_FIELD_REFERENCE_URL, - ecsReferenceUrl: ECS_REFERENCE_URL, - incompatible: partitionedFieldMetadata.incompatible.length, - indexName, - mappingUrl: MAPPING_URL, - }), + getSummaryMarkdownComment(indexName), getSummaryTableMarkdownComment({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }), getTabCountsMarkdownComment(partitionedFieldMetadata), incompatibleFieldsMarkdownComment, diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx index f4def1393c07c..2fa4fcdb4f8ed 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/incompatible_tab/index.tsx @@ -13,14 +13,12 @@ import { EuiEmptyPrompt, EuiSpacer, } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useCallback, useMemo } from 'react'; import { IncompatibleCallout } from '../callouts/incompatible_callout'; import { CompareFieldsTable } from '../../../compare_fields_table'; import { getIncompatibleMappingsTableColumns } from '../../../compare_fields_table/get_incompatible_mappings_table_columns'; import { getIncompatibleValuesTableColumns } from '../../../compare_fields_table/helpers'; -import { EMPTY_STAT } from '../../../helpers'; import { EmptyPromptBody } from '../../index_properties/empty_prompt_body'; import { EmptyPromptTitle } from '../../index_properties/empty_prompt_title'; import { @@ -41,31 +39,30 @@ import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; interface Props { addSuccessToast: (toast: { title: string }) => void; addToNewCaseDisabled: boolean; - defaultNumberFormat: string; docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; onAddToNewCase: (markdownComments: string[]) => void; partitionedFieldMetadata: PartitionedFieldMetadata; patternDocsCount: number; + sizeInBytes: number | undefined; } const IncompatibleTabComponent: React.FC = ({ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, ilmPhase, indexName, onAddToNewCase, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }) => { - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); const body = useMemo(() => , []); const title = useMemo(() => , []); const incompatibleMappings = useMemo( @@ -80,13 +77,24 @@ const IncompatibleTabComponent: React.FC = ({ () => getAllIncompatibleMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }), - [docsCount, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount] + [ + docsCount, + formatBytes, + formatNumber, + ilmPhase, + indexName, + partitionedFieldMetadata, + patternDocsCount, + sizeInBytes, + ] ); const onClickAddToCase = useCallback( () => onAddToNewCase([markdownComments.join('\n')]), @@ -101,7 +109,7 @@ const IncompatibleTabComponent: React.FC = ({ }, [addSuccessToast, markdownComments]); return ( - <> +
{showInvalidCallout(partitionedFieldMetadata.incompatible) ? ( <> @@ -161,7 +169,7 @@ const IncompatibleTabComponent: React.FC = ({ titleSize="s" /> )} - +
); }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx index cf83cf96d2812..2714d1002c40c 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/styles.tsx @@ -5,13 +5,43 @@ * 2.0. */ -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexItem, EuiLink } from '@elastic/eui'; import styled from 'styled-components'; +export const DEFAULT_LEGEND_HEIGHT = 300; // px +export const DEFAULT_MAX_CHART_HEIGHT = 300; // px + export const CalloutItem = styled.div` margin-left: ${({ theme }) => theme.eui.euiSizeS}; `; +export const ChartFlexItem = styled(EuiFlexItem)<{ + $maxChartHeight: number | undefined; + $minChartHeight: number; +}>` + ${({ $maxChartHeight }) => ($maxChartHeight != null ? `max-height: ${$maxChartHeight}px;` : '')} + min-height: ${({ $minChartHeight }) => `${$minChartHeight}px`}; +`; + export const CopyToClipboardButton = styled(EuiButtonEmpty)` margin-left: ${({ theme }) => theme.eui.euiSizeXS}; `; + +export const LegendContainer = styled.div<{ + $height?: number; + $width?: number; +}>` + margin-left: ${({ theme }) => theme.eui.euiSizeM}; + margin-top: ${({ theme }) => theme.eui.euiSizeM}; + ${({ $height }) => ($height != null ? `height: ${$height}px;` : '')} + scrollbar-width: thin; + ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} +`; + +export const StorageTreemapContainer = styled.div` + padding: ${({ theme }) => theme.eui.euiSizeM}; +`; + +export const ChartLegendLink = styled(EuiLink)` + width: 100%; +`; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx index 054ac5e003326..6d36fdd50370a 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/callout_summary/index.tsx @@ -6,14 +6,12 @@ */ import { copyToClipboard, EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import numeral from '@elastic/numeral'; import React, { useCallback, useMemo } from 'react'; import { MissingTimestampCallout } from '../../callouts/missing_timestamp_callout'; import { IncompatibleCallout } from '../../callouts/incompatible_callout'; import { showMissingTimestampCallout } from '../../helpers'; import { getMarkdownComments } from '../helpers'; -import { EMPTY_STAT } from '../../../../helpers'; import { showInvalidCallout } from '../../incompatible_tab/helpers'; import { CopyToClipboardButton } from '../../styles'; import * as i18n from '../../../index_properties/translations'; @@ -23,52 +21,55 @@ import type { IlmPhase, PartitionedFieldMetadata } from '../../../../types'; interface Props { addSuccessToast: (toast: { title: string }) => void; addToNewCaseDisabled: boolean; - defaultNumberFormat: string; docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; onAddToNewCase: (markdownComment: string[]) => void; partitionedFieldMetadata: PartitionedFieldMetadata; pattern: string; patternDocsCount: number; + sizeInBytes: number | undefined; } const CalloutSummaryComponent: React.FC = ({ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, docsCount, + formatBytes, + formatNumber, ilmPhase, indexName, onAddToNewCase, partitionedFieldMetadata, pattern, patternDocsCount, + sizeInBytes, }) => { - const formatNumber = useCallback( - (value: number | undefined): string => - value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, - [defaultNumberFormat] - ); const markdownComments: string[] = useMemo( () => getMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, pattern, patternDocsCount, + sizeInBytes, }), [ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, pattern, patternDocsCount, + sizeInBytes, ] ); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts new file mode 100644 index 0000000000000..64e78a4a88cfb --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.test.ts @@ -0,0 +1,235 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { EcsVersion } from '@kbn/ecs'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EMPTY_STAT } from '../../../helpers'; + +import { mockPartitionedFieldMetadata } from '../../../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { PartitionedFieldMetadata } from '../../../types'; +import { + ALL_TAB_ID, + CUSTOM_TAB_ID, + ECS_COMPLIANT_TAB_ID, + INCOMPATIBLE_TAB_ID, +} from '../../index_properties/helpers'; +import { + CUSTOM_FIELDS, + ECS_COMPLIANT_FIELDS, + INCOMPATIBLE_FIELDS, + UNKNOWN, +} from '../../index_properties/translations'; +import { + CategoryId, + getFillColor, + getMarkdownComments, + getNodeLabel, + getSummaryData, + getTabId, +} from './helpers'; + +describe('helpers', () => { + describe('getSummaryData', () => { + test('it returns the expected `SummaryData`', () => { + expect(getSummaryData(mockPartitionedFieldMetadata)).toEqual([ + { categoryId: 'incompatible', mappings: 3 }, + { categoryId: 'custom', mappings: 4 }, + { categoryId: 'ecs-compliant', mappings: 2 }, + ]); + }); + }); + + describe('getFillColor', () => { + const invalid: CategoryId = 'invalid-category-id' as CategoryId; + + const categories: Array<{ + categoryId: CategoryId; + expectedColor: string; + }> = [ + { + categoryId: 'incompatible', + expectedColor: euiThemeVars.euiColorDanger, + }, + { + categoryId: 'custom', + expectedColor: euiThemeVars.euiColorLightShade, + }, + { + categoryId: 'ecs-compliant', + expectedColor: euiThemeVars.euiColorSuccess, + }, + { + categoryId: invalid, + expectedColor: euiThemeVars.euiColorGhost, + }, + ]; + + categories.forEach(({ categoryId, expectedColor }) => { + test(`it returns the expected color for category '${categoryId}'`, () => { + expect(getFillColor(categoryId)).toEqual(expectedColor); + }); + }); + }); + + describe('getNodeLabel', () => { + const invalid: CategoryId = 'invalid-category-id' as CategoryId; + + const categories: Array<{ + categoryId: CategoryId; + expectedLabel: string; + }> = [ + { + categoryId: 'incompatible', + expectedLabel: INCOMPATIBLE_FIELDS, + }, + { + categoryId: 'custom', + expectedLabel: CUSTOM_FIELDS, + }, + { + categoryId: 'ecs-compliant', + expectedLabel: ECS_COMPLIANT_FIELDS, + }, + { + categoryId: invalid, + expectedLabel: UNKNOWN, + }, + ]; + + categories.forEach(({ categoryId, expectedLabel }) => { + test(`it returns the expected label for category '${categoryId}'`, () => { + expect(getNodeLabel(categoryId)).toEqual(expectedLabel); + }); + }); + }); + + describe('getTabId', () => { + const groupByFields: Array<{ + groupByField: string; + expectedTabId: string; + }> = [ + { + groupByField: 'incompatible', + expectedTabId: INCOMPATIBLE_TAB_ID, + }, + { + groupByField: 'custom', + expectedTabId: CUSTOM_TAB_ID, + }, + { + groupByField: 'ecs-compliant', + expectedTabId: ECS_COMPLIANT_TAB_ID, + }, + { + groupByField: 'some-other-group', + expectedTabId: ALL_TAB_ID, + }, + ]; + + groupByFields.forEach(({ groupByField, expectedTabId }) => { + test(`it returns the expected tab ID for groupByField '${groupByField}'`, () => { + expect(getTabId(groupByField)).toEqual(expectedTabId); + }); + }); + }); + + describe('getMarkdownComments', () => { + const defaultBytesFormat = '0,0.[0]b'; + const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + + const defaultNumberFormat = '0,0.[000]'; + const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + + test('it returns the expected comment when the index has incompatible fields ', () => { + expect( + getMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'auditbeat-*', + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + `#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ]); + }); + + test('it returns an empty array when the index does NOT have incompatible fields ', () => { + const noIncompatible: PartitionedFieldMetadata = { + ...mockPartitionedFieldMetadata, + incompatible: [], // <-- no incompatible fields + }; + + expect( + getMarkdownComments({ + docsCount: 4, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-index-1', + partitionedFieldMetadata: noIncompatible, + pattern: 'auditbeat-*', + patternDocsCount: 57410, + sizeInBytes: 28413, + }) + ).toEqual([]); + }); + + test('it returns a missing timestamp comment for an empty index', () => { + const emptyIndex: PartitionedFieldMetadata = { + all: [], + ecsCompliant: [], + custom: [], + incompatible: [ + { + description: + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', + hasEcsMetadata: true, + indexFieldName: '@timestamp', + indexFieldType: '-', + indexInvalidValues: [], + isEcsCompliant: false, + isInSameFamily: false, + type: 'date', + }, + ], + }; + + expect( + getMarkdownComments({ + docsCount: 0, + formatBytes, + formatNumber, + ilmPhase: 'unmanaged', + indexName: 'auditbeat-custom-empty-index-1', + partitionedFieldMetadata: emptyIndex, + pattern: 'auditbeat-*', + patternDocsCount: 57410, + sizeInBytes: 247, + }) + ).toEqual([ + '### auditbeat-custom-empty-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-empty-index-1 | 0 (0.0%) | 1 | `unmanaged` | 247B |\n\n', + '### **Incompatible fields** `1` **Custom fields** `0` **ECS compliant fields** `0` **All fields** `0`\n', + `#### 1 incompatible field, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n`, + '\n#### Incompatible field mappings - auditbeat-custom-empty-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| @timestamp | `date` | `-` |\n\n\n', + '#### Missing an @timestamp (date) field mapping for this index\n\nConsider adding an @timestamp (date) field mapping to this index, as required by the Elastic Common Schema (ECS), because:\n\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n', + ]); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts index 4ab85f87e01d0..1f728e3b60c86 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/helpers.ts @@ -79,28 +79,34 @@ const isString = (x: string | null): x is string => typeof x === 'string'; export const getMarkdownComments = ({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }: { docsCount: number; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; ilmPhase: IlmPhase | undefined; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata; pattern: string; patternDocsCount: number; + sizeInBytes: number | undefined; }): string[] => { const invalidMarkdownComments = showInvalidCallout(partitionedFieldMetadata.incompatible) ? getAllIncompatibleMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }) : []; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx index 087470b7e86dc..c830ac2f6c7be 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/data_quality_panel/tabs/summary_tab/index.tsx @@ -24,8 +24,9 @@ import type { IlmPhase, PartitionedFieldMetadata } from '../../../types'; interface Props { addSuccessToast: (toast: { title: string }) => void; addToNewCaseDisabled: boolean; - defaultNumberFormat: string; docsCount: number; + formatBytes: (value: number | undefined) => string; + formatNumber: (value: number | undefined) => string; getGroupByFieldsOnClick: ( elements: Array< | FlameElementEvent @@ -46,13 +47,15 @@ interface Props { pattern: string; patternDocsCount: number; setSelectedTabId: (tabId: string) => void; + sizeInBytes: number | undefined; theme: Theme; } const SummaryTabComponent: React.FC = ({ addSuccessToast, addToNewCaseDisabled, - defaultNumberFormat, + formatBytes, + formatNumber, docsCount, getGroupByFieldsOnClick, ilmPhase, @@ -62,13 +65,15 @@ const SummaryTabComponent: React.FC = ({ pattern, patternDocsCount, setSelectedTabId, + sizeInBytes, theme, }) => ( <> = ({ partitionedFieldMetadata={partitionedFieldMetadata} pattern={pattern} patternDocsCount={patternDocsCount} + sizeInBytes={sizeInBytes} /> void; + color: string | null; + count: number | string; + dataTestSubj?: string; + onClick: (() => void) | undefined; text: string; + textWidth?: number; } -const ChartLegendItemComponent: React.FC = ({ color, count, onClick, text }) => { - return ( +const ChartLegendItemComponent: React.FC = ({ + color, + count, + dataTestSubj = DEFAULT_DATA_TEST_SUBJ, + onClick, + text, + textWidth, +}) => ( + - - - {text} - - + + {color != null ? ( + + + {text} + + + ) : ( + + {text} + + )} + + - -
{count}
-
+ {count}
- ); -}; +
+); ChartLegendItemComponent.displayName = 'ChartLegendItemComponent'; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx index 21dd3e0450f40..b7c05e75300d5 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/chart_legend/index.tsx @@ -5,9 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback } from 'react'; -import styled from 'styled-components'; import { ChartLegendItem } from './chart_legend_item'; import { getEcsCompliantColor } from '../../data_quality_panel/tabs/helpers'; @@ -20,10 +18,9 @@ import { getCustomColor } from '../../data_quality_panel/tabs/custom_tab/helpers import { getIncompatibleColor } from '../../data_quality_panel/tabs/incompatible_tab/helpers'; import type { PartitionedFieldMetadata } from '../../types'; import * as i18n from '../../data_quality_panel/index_properties/translations'; +import { LegendContainer } from '../../data_quality_panel/tabs/styles'; -const ChartLegendFlexGroup = styled(EuiFlexGroup)` - width: 210px; -`; +const LEGEND_WIDTH = 200; // px interface Props { partitionedFieldMetadata: PartitionedFieldMetadata; @@ -44,40 +41,34 @@ const ChartLegendComponent: React.FC = ({ partitionedFieldMetadata, setSe ); return ( - + {partitionedFieldMetadata.incompatible.length > 0 && ( - - - + )} {partitionedFieldMetadata.custom.length > 0 && ( - - - + )} {partitionedFieldMetadata.ecsCompliant.length > 0 && ( - - - + )} - + ); }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts new file mode 100644 index 0000000000000..30a5e9d13e4fc --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ecs_summary_donut_chart/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { allMetadataIsEmpty } from './helpers'; +import { mockPartitionedFieldMetadata } from '../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; +import { PartitionedFieldMetadata } from '../types'; + +describe('helpers', () => { + describe('allMetadataIsEmpty', () => { + test('it returns false when `all` is NOT is empty', () => { + expect(allMetadataIsEmpty(mockPartitionedFieldMetadata)).toBe(false); + }); + + test('it returns true when `all` is is empty', () => { + const allIsEmpty: PartitionedFieldMetadata = { + all: [], // <-- empty + custom: [], + ecsCompliant: [], + incompatible: [], + }; + + expect(allMetadataIsEmpty(allIsEmpty)).toBe(true); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.test.ts index 14448d1a3baac..81e968bfe73a4 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.test.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - IlmExplainLifecycleLifecycleExplain, - MappingProperty, -} from '@elastic/elasticsearch/lib/api/types'; +import { IlmExplainLifecycleLifecycleExplain } from '@elastic/elasticsearch/lib/api/types'; import { EcsFlat } from '@kbn/ecs'; import { omit } from 'lodash/fp'; @@ -28,9 +25,11 @@ import { getMissingTimestampFieldMetadata, getPartitionedFieldMetadata, getPartitionedFieldMetadataStats, + getSizeInBytes, getTotalDocsCount, getTotalPatternIncompatible, getTotalPatternIndicesChecked, + getTotalSizeInBytes, hasValidTimestampMapping, isMappingCompatible, } from './helpers'; @@ -44,8 +43,9 @@ import { sourcePort, timestamp, eventCategoryWithUnallowedValues, -} from './mock/enriched_field_metadata'; -import { mockIlmExplain } from './mock/ilm_explain'; +} from './mock/enriched_field_metadata/mock_enriched_field_metadata'; +import { mockIlmExplain } from './mock/ilm_explain/mock_ilm_explain'; +import { mockMappingsProperties } from './mock/mappings_properties/mock_mappings_properties'; import { alertIndexNoResults } from './mock/pattern_rollup/mock_alerts_pattern_rollup'; import { packetbeatNoResults, @@ -79,7 +79,7 @@ const ecsMetadata: Record = EcsFlat as unknown as Record { describe('getIndexNames', () => { - const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; + const ilmPhases = ['hot', 'warm', 'unmanaged']; test('returns the expected index names when they have an ILM phase included in the ilmPhases list', () => { expect( @@ -91,17 +91,18 @@ describe('helpers', () => { ).toEqual([ '.ds-packetbeat-8.6.1-2023.02.04-000001', '.ds-packetbeat-8.5.3-2023.02.04-000001', + 'auditbeat-custom-index-1', ]); }); test('returns the expected filtered index names when they do NOT have an ILM phase included in the ilmPhases list', () => { expect( getIndexNames({ - ilmExplain: mockIlmExplain, // <-- the mock indexes have 'hot' ILM phases... + ilmExplain: mockIlmExplain, // <-- the mock indexes have 'hot' and 'unmanaged' ILM phases... ilmPhases: ['warm', 'unmanaged'], // <-- ...but we don't ask for 'hot' stats: mockStats, }) - ).toEqual([]); + ).toEqual(['auditbeat-custom-index-1']); // <-- the 'unmanaged' index }); test('returns the expected index names when the `ilmExplain` is missing a record for an index', () => { @@ -117,7 +118,7 @@ describe('helpers', () => { ilmPhases: ['hot', 'warm', 'unmanaged'], stats: mockStats, }) - ).toEqual(['.ds-packetbeat-8.5.3-2023.02.04-000001']); // <-- only includes one of the two indexes, because the other one is missing an ILM explain record + ).toEqual(['.ds-packetbeat-8.5.3-2023.02.04-000001', 'auditbeat-custom-index-1']); // <-- only includes two of the three indices, because the other one is missing an ILM explain record }); test('returns empty index names when `ilmPhases` is empty', () => { @@ -162,105 +163,6 @@ describe('helpers', () => { }); describe('getFieldTypes', () => { - /** - * These `mappingsProperties` represent mappings that were generated by - * Elasticsearch automatically, for an index named `auditbeat-custom-index-1`: - * - * ``` - * DELETE auditbeat-custom-index-1 - * - * PUT auditbeat-custom-index-1 - * - * PUT auditbeat-custom-index-1/_mapping - * { - * "properties": { - * "@timestamp": { - * "type": "date" - * }, - * "event.category": { - * "type": "keyword", - * "ignore_above": 1024 - * } - * } - * } - * ``` - * - * when the following document was inserted: - * - * ``` - * POST auditbeat-custom-index-1/_doc - * { - * "@timestamp": "2023-02-06T09:41:49.668Z", - * "host": { - * "name": "foo" - * }, - * "event": { - * "category": "an_invalid_category" - * }, - * "some.field": "this", - * "source": { - * "port": 90210, - * "ip": "10.1.2.3" - * } - * } - * ``` - */ - const mappingsProperties: Record = { - '@timestamp': { - type: 'date', - }, - event: { - properties: { - category: { - type: 'keyword', - ignore_above: 1024, - }, - }, - }, - host: { - properties: { - name: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - ignore_above: 256, - }, - }, - }, - }, - }, - some: { - properties: { - field: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - ignore_above: 256, - }, - }, - }, - }, - }, - source: { - properties: { - ip: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - ignore_above: 256, - }, - }, - }, - port: { - type: 'long', - }, - }, - }, - }; - const expected = [ { field: '@timestamp', @@ -301,7 +203,7 @@ describe('helpers', () => { ]; test('it flattens the field names and types in the mapping properties', () => { - expect(getFieldTypes(mappingsProperties)).toEqual(expected); + expect(getFieldTypes(mockMappingsProperties)).toEqual(expected); }); test('it throws a type error when mappingsProperties is not flatten-able', () => { @@ -876,6 +778,53 @@ describe('helpers', () => { }); }); + describe('getSizeInBytes', () => { + test('it returns the expected size when `stats` contains the `indexName`', () => { + const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + const expectedCount = mockStatsYellowIndex[indexName].primaries?.store?.size_in_bytes; + + expect( + getSizeInBytes({ + indexName, + stats: mockStatsYellowIndex, + }) + ).toEqual(expectedCount); + }); + + test('it returns zero when `stats` does NOT contain the `indexName`', () => { + const indexName = 'not-gonna-find-it'; + + expect( + getSizeInBytes({ + indexName, + stats: mockStatsYellowIndex, + }) + ).toEqual(0); + }); + + test('it returns zero when `stats` is null', () => { + const indexName = '.ds-packetbeat-8.6.1-2023.02.04-000001'; + + expect( + getSizeInBytes({ + indexName, + stats: null, + }) + ).toEqual(0); + }); + + test('it returns the expected size for a green index, where `primaries.store.size_in_bytes` and `total.store.size_in_bytes` have different values', () => { + const indexName = 'auditbeat-custom-index-1'; + + expect( + getSizeInBytes({ + indexName, + stats: mockStatsGreenIndex, + }) + ).toEqual(mockStatsGreenIndex[indexName].primaries?.store?.size_in_bytes); + }); + }); + describe('getTotalDocsCount', () => { test('it returns the expected total given a subset of index names in the stats', () => { const indexName = '.ds-packetbeat-8.5.3-2023.02.04-000001'; @@ -925,6 +874,55 @@ describe('helpers', () => { }); }); + describe('getTotalSizeInBytes', () => { + test('it returns the expected total given a subset of index names in the stats', () => { + const indexName = '.ds-packetbeat-8.5.3-2023.02.04-000001'; + const expectedCount = mockStatsYellowIndex[indexName].primaries?.store?.size_in_bytes; + + expect( + getTotalSizeInBytes({ + indexNames: [indexName], + stats: mockStatsYellowIndex, + }) + ).toEqual(expectedCount); + }); + + test('it returns the expected total given all index names in the stats', () => { + const allIndexNamesInStats = [ + '.ds-packetbeat-8.6.1-2023.02.04-000001', + '.ds-packetbeat-8.5.3-2023.02.04-000001', + ]; + + expect( + getTotalSizeInBytes({ + indexNames: allIndexNamesInStats, + stats: mockStatsYellowIndex, + }) + ).toEqual(1464758182); + }); + + test('it returns zero given an empty collection of index names', () => { + expect( + getTotalSizeInBytes({ + indexNames: [], // <-- empty + stats: mockStatsYellowIndex, + }) + ).toEqual(0); + }); + + test('it returns the expected total for a green index', () => { + const indexName = 'auditbeat-custom-index-1'; + const expectedCount = mockStatsGreenIndex[indexName].primaries?.store?.size_in_bytes; + + expect( + getTotalSizeInBytes({ + indexNames: [indexName], + stats: mockStatsGreenIndex, + }) + ).toEqual(expectedCount); + }); + }); + describe('getIlmPhaseDescription', () => { const phases: Array<{ phase: string; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.ts index ea8a50a41580f..7cb638ad11550 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/helpers.ts @@ -255,6 +255,14 @@ export const getDocsCount = ({ stats: Record | null; }): number => (stats && stats[indexName]?.primaries?.docs?.count) ?? 0; +export const getSizeInBytes = ({ + indexName, + stats, +}: { + indexName: string; + stats: Record | null; +}): number => (stats && stats[indexName]?.primaries?.store?.size_in_bytes) ?? 0; + export const getTotalDocsCount = ({ indexNames, stats, @@ -267,6 +275,18 @@ export const getTotalDocsCount = ({ 0 ); +export const getTotalSizeInBytes = ({ + indexNames, + stats, +}: { + indexNames: string[]; + stats: Record | null; +}): number => + indexNames.reduce( + (acc: number, indexName: string) => acc + getSizeInBytes({ stats, indexName }), + 0 + ); + export const EMPTY_STAT = '--'; /** diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx new file mode 100644 index 0000000000000..417f1419a7ca5 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/ilm_phases_empty_prompt/index.test.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../mock/test_providers/test_providers'; +import { IlmPhasesEmptyPrompt } from '.'; + +describe('IlmPhasesEmptyPrompt', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it renders the expected content', () => { + expect(screen.getByTestId('ilmPhasesEmptyPrompt')).toHaveTextContent( + "ILM phases that can be checked for data qualityhot: The index is actively being updated and queriedwarm: The index is no longer being updated but is still being queriedunmanaged: The index isn't managed by Index Lifecycle Management (ILM)ILM phases that cannot be checkedThe following ILM phases cannot be checked for data quality because they are slower to accesscold: The index is no longer being updated and is queried infrequently. The information still needs to be searchable, but it’s okay if those queries are slower.frozen: The index is no longer being updated and is queried rarely. The information still needs to be searchable, but it's okay if those queries are extremely slow." + ); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.test.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.test.tsx index 72affc8aec491..5f6711814a904 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.test.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.test.tsx @@ -9,12 +9,12 @@ import { DARK_THEME } from '@elastic/charts'; import { render, screen } from '@testing-library/react'; import React from 'react'; -import { TestProviders } from './mock/test_providers'; +import { TestProviders } from './mock/test_providers/test_providers'; import { DataQualityPanel } from '.'; describe('DataQualityPanel', () => { - describe('when no ILM phases are provided', () => { - const ilmPhases: string[] = []; + describe('when ILM phases are provided', () => { + const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; beforeEach(() => { render( @@ -22,6 +22,7 @@ describe('DataQualityPanel', () => { { ); }); - test('it renders the ILM phases empty prompt', () => { - expect(screen.getByTestId('ilmPhasesEmptyPrompt')).toBeInTheDocument(); + test('it does NOT render the ILM phases empty prompt', () => { + expect(screen.queryByTestId('ilmPhasesEmptyPrompt')).not.toBeInTheDocument(); }); - test('it does NOT render the body', () => { - expect(screen.queryByTestId('body')).not.toBeInTheDocument(); + test('it renders the body', () => { + expect(screen.getByTestId('body')).toBeInTheDocument(); }); }); - describe('when ILM phases are provided', () => { - const ilmPhases: string[] = ['hot', 'warm', 'unmanaged']; + describe('when ILM phases are NOT provided', () => { + test('it renders the ILM phases empty prompt', () => { + const ilmPhases: string[] = []; - beforeEach(() => { render( { /> ); - }); - - test('it does NOT render the ILM phases empty prompt', () => { - expect(screen.queryByTestId('ilmPhasesEmptyPrompt')).not.toBeInTheDocument(); - }); - test('it renders the body', () => { - expect(screen.getByTestId('body')).toBeInTheDocument(); + expect(screen.getByTestId('ilmPhasesEmptyPrompt')).toBeInTheDocument(); }); }); }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.tsx index 05ccaf9c4fc5e..dde54d8e97f64 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/index.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import numeral from '@elastic/numeral'; import type { FlameElementEvent, HeatmapElementEvent, @@ -14,15 +15,16 @@ import type { WordCloudElementEvent, XYChartElementEvent, } from '@elastic/charts'; -import React from 'react'; +import React, { useCallback } from 'react'; import { Body } from './data_quality_panel/body'; -import { IlmPhasesEmptyPrompt } from './ilm_phases_empty_prompt'; +import { EMPTY_STAT } from './helpers'; interface Props { addSuccessToast: (toast: { title: string }) => void; canUserCreateAndReadCases: () => boolean; defaultNumberFormat: string; + defaultBytesFormat: string; getGroupByFieldsOnClick: ( elements: Array< | FlameElementEvent @@ -54,6 +56,7 @@ interface Props { const DataQualityPanelComponent: React.FC = ({ addSuccessToast, canUserCreateAndReadCases, + defaultBytesFormat, defaultNumberFormat, getGroupByFieldsOnClick, ilmPhases, @@ -63,15 +66,24 @@ const DataQualityPanelComponent: React.FC = ({ setLastChecked, theme, }) => { - if (ilmPhases.length === 0) { - return ; - } + const formatBytes = useCallback( + (value: number | undefined): string => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT, + [defaultBytesFormat] + ); + + const formatNumber = useCallback( + (value: number | undefined): string => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT, + [defaultNumberFormat] + ); return ( = { + 'auditbeat-custom-index-1': { + docsCount: 4, + error: null, + ilmPhase: 'unmanaged', + incompatible: 3, + indexName: 'auditbeat-custom-index-1', + markdownComments: [ + '### auditbeat-custom-index-1\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - auditbeat-custom-index-1\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - auditbeat-custom-index-1\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2),\n`theory` (1) |\n\n', + ], + pattern: 'auditbeat-*', + }, + 'auditbeat-7.9.3-2023.02.13-000001': { + docsCount: 2438, + error: null, + ilmPhase: 'hot', + incompatible: 12, + indexName: 'auditbeat-7.9.3-2023.02.13-000001', + markdownComments: [ + '### auditbeat-7.9.3-2023.02.13-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase |\n|--------|-------|------|---------------------|-----------|\n| ❌ | auditbeat-7.9.3-2023.02.13-000001 | 2,438 (4.2%) | 12 | `hot` |\n\n', + '### **Incompatible fields** `12` **Custom fields** `439` **ECS compliant fields** `506` **All fields** `957`\n', + "#### 12 incompatible fields, 11 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - auditbeat-7.9.3-2023.02.13-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| error.message | `match_only_text` | `text` `same family` |\n| error.stack_trace | `wildcard` | `keyword` `same family` |\n| http.request.body.content | `wildcard` | `keyword` `same family` |\n| http.response.body.content | `wildcard` | `keyword` `same family` |\n| message | `match_only_text` | `text` `same family` |\n| process.command_line | `wildcard` | `keyword` `same family` |\n| process.parent.command_line | `wildcard` | `keyword` `same family` |\n| registry.data.strings | `wildcard` | `keyword` `same family` |\n| url.full | `wildcard` | `keyword` `same family` |\n| url.original | `wildcard` | `keyword` `same family` |\n| url.path | `wildcard` | `keyword` `same family` |\n\n#### Incompatible field values - auditbeat-7.9.3-2023.02.13-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.kind | `alert`, `enrichment`, `event`, `metric`, `state`, `pipeline_error`, `signal` | `error` (7) |\n\n', + ], + pattern: 'auditbeat-*', + }, +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/index.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/mock_enriched_field_metadata.ts similarity index 87% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/index.ts rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/mock_enriched_field_metadata.ts index 654d608d7d351..be445b6ee6b51 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/index.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/enriched_field_metadata/mock_enriched_field_metadata.ts @@ -270,3 +270,75 @@ export const sourcePort: EnrichedFieldMetadata = { isEcsCompliant: true, isInSameFamily: false, // `long` is not a member of any families }; + +export const mockCustomFields: EnrichedFieldMetadata[] = [ + { + indexFieldName: 'host.name.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'some.field.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + indexFieldName: 'source.ip.keyword', + indexFieldType: 'keyword', + indexInvalidValues: [], + hasEcsMetadata: false, + isEcsCompliant: false, + isInSameFamily: false, + }, +]; + +export const mockIncompatibleMappings: EnrichedFieldMetadata[] = [ + { + dashed_name: 'host-name', + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + flat_name: 'host.name', + ignore_above: 1024, + level: 'core', + name: 'name', + normalize: [], + short: 'Name of the host.', + type: 'keyword', + indexFieldName: 'host.name', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, + { + dashed_name: 'source-ip', + description: 'IP address of the source (IPv4 or IPv6).', + flat_name: 'source.ip', + level: 'core', + name: 'ip', + normalize: [], + short: 'IP address of the source.', + type: 'ip', + indexFieldName: 'source.ip', + indexFieldType: 'text', + indexInvalidValues: [], + hasEcsMetadata: true, + isEcsCompliant: false, + isInSameFamily: false, + }, +]; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/index.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/mock_ilm_explain.ts similarity index 94% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/index.ts rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/mock_ilm_explain.ts index bf7bc667c9aaf..b5b35064a7320 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/index.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/ilm_explain/mock_ilm_explain.ts @@ -48,4 +48,8 @@ export const mockIlmExplain: Record modified_date_in_millis: 1675536751205, }, }, + 'auditbeat-custom-index-1': { + index: 'auditbeat-custom-index-1', + managed: false, + }, }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record.ts new file mode 100644 index 0000000000000..e63bae0530961 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/indices_get_mapping_index_mapping_record/mock_indices_get_mapping_index_mapping_record.ts @@ -0,0 +1,73 @@ +/* + * 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 { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types'; + +export const mockIndicesGetMappingIndexMappingRecords: Record< + string, + IndicesGetMappingIndexMappingRecord +> = { + 'auditbeat-custom-index-1': { + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + event: { + properties: { + category: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + some: { + properties: { + field: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + source: { + properties: { + ip: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + port: { + type: 'long', + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_properties/mock_mappings_properties.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_properties/mock_mappings_properties.ts new file mode 100644 index 0000000000000..42b22d9c99aaa --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_properties/mock_mappings_properties.ts @@ -0,0 +1,107 @@ +/* + * 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 { MappingProperty } from '@elastic/elasticsearch/lib/api/types'; + +/** + * These `mappingsProperties` represent mappings that were generated by + * Elasticsearch automatically, for an index named `auditbeat-custom-index-1`: + * + * ``` + * DELETE auditbeat-custom-index-1 + * + * PUT auditbeat-custom-index-1 + * + * PUT auditbeat-custom-index-1/_mapping + * { + * "properties": { + * "@timestamp": { + * "type": "date" + * }, + * "event.category": { + * "type": "keyword", + * "ignore_above": 1024 + * } + * } + * } + * ``` + * + * when the following document was inserted: + * + * ``` + * POST auditbeat-custom-index-1/_doc + * { + * "@timestamp": "2023-02-06T09:41:49.668Z", + * "host": { + * "name": "foo" + * }, + * "event": { + * "category": "an_invalid_category" + * }, + * "some.field": "this", + * "source": { + * "port": 90210, + * "ip": "10.1.2.3" + * } + * } + * ``` + */ +export const mockMappingsProperties: Record = { + '@timestamp': { + type: 'date', + }, + event: { + properties: { + category: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + some: { + properties: { + field: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + source: { + properties: { + ip: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + port: { + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_response/mock_mappings_response.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_response/mock_mappings_response.ts new file mode 100644 index 0000000000000..5e15bb4e2efdd --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/mappings_response/mock_mappings_response.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const mockMappingsResponse = { + 'auditbeat-custom-index-1': { + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + event: { + properties: { + category: { + type: 'keyword', + ignore_above: 1024, + }, + }, + }, + host: { + properties: { + name: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + some: { + properties: { + field: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + }, + }, + source: { + properties: { + ip: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 256, + }, + }, + }, + port: { + type: 'long', + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_alerts_pattern_rollup.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_alerts_pattern_rollup.ts index cbca7ab9e1965..39c25cbc77c10 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_alerts_pattern_rollup.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_alerts_pattern_rollup.ts @@ -33,6 +33,7 @@ export const alertIndexNoResults: PatternRollup = { indices: 1, pattern: '.alerts-security.alerts-default', results: undefined, // <-- no results + sizeInBytes: 6423408623, stats: { '.internal.alerts-security.alerts-default-000001': { health: 'green', @@ -83,6 +84,7 @@ export const alertIndexWithAllResults: PatternRollup = { pattern: '.alerts-security.alerts-default', }, }, + sizeInBytes: 29717961631, stats: { '.internal.alerts-security.alerts-default-000001': { health: 'green', diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_auditbeat_pattern_rollup.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_auditbeat_pattern_rollup.ts index 776f02f9f4e74..3ece4fb4c248f 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_auditbeat_pattern_rollup.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_auditbeat_pattern_rollup.ts @@ -45,10 +45,17 @@ export const auditbeatNoResults: PatternRollup = { indices: 3, pattern: 'auditbeat-*', results: undefined, // <-- no results + sizeInBytes: 18820446, stats: { '.ds-auditbeat-8.6.1-2023.02.07-000001': { uuid: 'YpxavlUVTw2x_E_QtADrpg', health: 'yellow', + primaries: { + store: { + size_in_bytes: 18791790, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -60,6 +67,12 @@ export const auditbeatNoResults: PatternRollup = { 'auditbeat-custom-empty-index-1': { uuid: 'Iz5FJjsLQla34mD6kBAQBw', health: 'yellow', + primaries: { + store: { + size_in_bytes: 247, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -71,6 +84,12 @@ export const auditbeatNoResults: PatternRollup = { 'auditbeat-custom-index-1': { uuid: 'xJvgb2QCQPSjlr7UnW8tFA', health: 'yellow', + primaries: { + store: { + size_in_bytes: 28409, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -152,10 +171,17 @@ export const auditbeatWithAllResults: PatternRollup = { pattern: 'auditbeat-*', }, }, + sizeInBytes: 18820446, stats: { '.ds-auditbeat-8.6.1-2023.02.07-000001': { uuid: 'YpxavlUVTw2x_E_QtADrpg', health: 'yellow', + primaries: { + store: { + size_in_bytes: 18791790, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -167,6 +193,12 @@ export const auditbeatWithAllResults: PatternRollup = { 'auditbeat-custom-empty-index-1': { uuid: 'Iz5FJjsLQla34mD6kBAQBw', health: 'yellow', + primaries: { + store: { + size_in_bytes: 247, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -178,6 +210,12 @@ export const auditbeatWithAllResults: PatternRollup = { 'auditbeat-custom-index-1': { uuid: 'xJvgb2QCQPSjlr7UnW8tFA', health: 'yellow', + primaries: { + store: { + size_in_bytes: 28409, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_packetbeat_pattern_rollup.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_packetbeat_pattern_rollup.ts index 2b39901b9c954..f26ef180a3641 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_packetbeat_pattern_rollup.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/pattern_rollup/mock_packetbeat_pattern_rollup.ts @@ -43,10 +43,17 @@ export const packetbeatNoResults: PatternRollup = { indices: 2, pattern: 'packetbeat-*', results: undefined, + sizeInBytes: 1096520898, stats: { '.ds-packetbeat-8.6.1-2023.02.04-000001': { uuid: 'x5Uuw4j4QM2YidHLNixCwg', health: 'yellow', + primaries: { + store: { + size_in_bytes: 512194751, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -58,6 +65,12 @@ export const packetbeatNoResults: PatternRollup = { '.ds-packetbeat-8.5.3-2023.02.04-000001': { uuid: 'we0vNWm2Q6iz6uHubyHS6Q', health: 'yellow', + primaries: { + store: { + size_in_bytes: 584326147, + reserved_in_bytes: 0, + }, + }, status: 'open', total: { docs: { @@ -127,27 +140,650 @@ export const packetbeatWithSomeErrors: PatternRollup = { pattern: 'packetbeat-*', }, }, + sizeInBytes: 1096520898, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + uuid: 'x5Uuw4j4QM2YidHLNixCwg', + health: 'yellow', + primaries: { + store: { + size_in_bytes: 512194751, + reserved_in_bytes: 0, + }, + }, + status: 'open', + total: { + docs: { + count: 1628343, + deleted: 0, + }, + }, + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + uuid: 'we0vNWm2Q6iz6uHubyHS6Q', + health: 'yellow', + primaries: { + store: { + size_in_bytes: 584326147, + reserved_in_bytes: 0, + }, + }, + status: 'open', + total: { + docs: { + count: 1630289, + deleted: 0, + }, + }, + }, + }, +}; + +export const mockPacketbeatPatternRollup: PatternRollup = { + docsCount: 3258632, + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: undefined, + sizeInBytes: 1464758182, stats: { '.ds-packetbeat-8.6.1-2023.02.04-000001': { uuid: 'x5Uuw4j4QM2YidHLNixCwg', health: 'yellow', status: 'open', + primaries: { + docs: { + count: 1628343, + deleted: 0, + }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 731583142, + total_data_set_size_in_bytes: 731583142, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 120726, + query_time_in_millis: 234865, + query_current: 0, + fetch_total: 109324, + fetch_time_in_millis: 500584, + fetch_current: 0, + scroll_total: 10432, + scroll_time_in_millis: 3874632, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 1, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 1, + }, + query_cache: { + memory_size_in_bytes: 8316098, + total_count: 34248343, + hit_count: 3138879, + miss_count: 31109464, + cache_size: 4585, + cache_count: 4585, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 12424, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 19, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 304, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 606298841, + }, + request_cache: { + memory_size_in_bytes: 89216, + evictions: 0, + hit_count: 704, + miss_count: 38, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, + }, total: { docs: { count: 1628343, deleted: 0, }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 731583142, + total_data_set_size_in_bytes: 731583142, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 120726, + query_time_in_millis: 234865, + query_current: 0, + fetch_total: 109324, + fetch_time_in_millis: 500584, + fetch_current: 0, + scroll_total: 10432, + scroll_time_in_millis: 3874632, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 1, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 1, + }, + query_cache: { + memory_size_in_bytes: 8316098, + total_count: 34248343, + hit_count: 3138879, + miss_count: 31109464, + cache_size: 4585, + cache_count: 4585, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 12424, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 19, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 304, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 606298841, + }, + request_cache: { + memory_size_in_bytes: 89216, + evictions: 0, + hit_count: 704, + miss_count: 38, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, }, }, '.ds-packetbeat-8.5.3-2023.02.04-000001': { uuid: 'we0vNWm2Q6iz6uHubyHS6Q', health: 'yellow', status: 'open', + primaries: { + docs: { + count: 1630289, + deleted: 0, + }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 733175040, + total_data_set_size_in_bytes: 733175040, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 120726, + query_time_in_millis: 248138, + query_current: 0, + fetch_total: 109484, + fetch_time_in_millis: 500514, + fetch_current: 0, + scroll_total: 10432, + scroll_time_in_millis: 3871379, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 2, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 1, + }, + query_cache: { + memory_size_in_bytes: 5387543, + total_count: 24212135, + hit_count: 2223357, + miss_count: 21988778, + cache_size: 3275, + cache_count: 3275, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 12336, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 20, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 320, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 606298805, + }, + request_cache: { + memory_size_in_bytes: 89320, + evictions: 0, + hit_count: 704, + miss_count: 38, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, + }, total: { docs: { count: 1630289, deleted: 0, }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 733175040, + total_data_set_size_in_bytes: 733175040, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 120726, + query_time_in_millis: 248138, + query_current: 0, + fetch_total: 109484, + fetch_time_in_millis: 500514, + fetch_current: 0, + scroll_total: 10432, + scroll_time_in_millis: 3871379, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 2, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 1, + }, + query_cache: { + memory_size_in_bytes: 5387543, + total_count: 24212135, + hit_count: 2223357, + miss_count: 21988778, + cache_size: 3275, + cache_count: 3275, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 12336, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 20, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 320, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 606298805, + }, + request_cache: { + memory_size_in_bytes: 89320, + evictions: 0, + hit_count: 704, + miss_count: 38, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, }, }, }, diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/stats/mock_stats.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/stats/mock_stats.tsx index 0362dcd70a53f..14465e815ad47 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/stats/mock_stats.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/stats/mock_stats.tsx @@ -558,4 +558,279 @@ export const mockStats: Record = { }, }, }, + 'auditbeat-custom-index-1': { + uuid: 'uyJDDqGrRQqdBTN0mCF-iw', + health: 'yellow', + status: 'open', + primaries: { + docs: { + count: 4, + deleted: 0, + }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 28413, + total_data_set_size_in_bytes: 28413, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 24, + query_time_in_millis: 5, + query_current: 0, + fetch_total: 24, + fetch_time_in_millis: 0, + fetch_current: 0, + scroll_total: 0, + scroll_time_in_millis: 0, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 0, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 0, + }, + query_cache: { + memory_size_in_bytes: 58, + total_count: 0, + hit_count: 0, + miss_count: 0, + cache_size: 0, + cache_count: 0, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 608, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 4, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 0, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 79289897, + }, + request_cache: { + memory_size_in_bytes: 3760, + evictions: 0, + hit_count: 20, + miss_count: 4, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, + }, + total: { + docs: { + count: 4, + deleted: 0, + }, + shard_stats: { + total_count: 1, + }, + store: { + size_in_bytes: 28413, + total_data_set_size_in_bytes: 28413, + reserved_in_bytes: 0, + }, + indexing: { + index_total: 0, + index_time_in_millis: 0, + index_current: 0, + index_failed: 0, + delete_total: 0, + delete_time_in_millis: 0, + delete_current: 0, + noop_update_total: 0, + is_throttled: false, + throttle_time_in_millis: 0, + }, + get: { + total: 0, + time_in_millis: 0, + exists_total: 0, + exists_time_in_millis: 0, + missing_total: 0, + missing_time_in_millis: 0, + current: 0, + }, + search: { + open_contexts: 0, + query_total: 24, + query_time_in_millis: 5, + query_current: 0, + fetch_total: 24, + fetch_time_in_millis: 0, + fetch_current: 0, + scroll_total: 0, + scroll_time_in_millis: 0, + scroll_current: 0, + suggest_total: 0, + suggest_time_in_millis: 0, + suggest_current: 0, + }, + merges: { + current: 0, + current_docs: 0, + current_size_in_bytes: 0, + total: 0, + total_time_in_millis: 0, + total_docs: 0, + total_size_in_bytes: 0, + total_stopped_time_in_millis: 0, + total_throttled_time_in_millis: 0, + total_auto_throttle_in_bytes: 20971520, + }, + refresh: { + total: 2, + total_time_in_millis: 0, + external_total: 2, + external_total_time_in_millis: 0, + listeners: 0, + }, + flush: { + total: 1, + periodic: 1, + total_time_in_millis: 0, + }, + warmer: { + current: 0, + total: 1, + total_time_in_millis: 0, + }, + query_cache: { + memory_size_in_bytes: 58, + total_count: 0, + hit_count: 0, + miss_count: 0, + cache_size: 0, + cache_count: 0, + evictions: 0, + }, + fielddata: { + memory_size_in_bytes: 608, + evictions: 0, + }, + completion: { + size_in_bytes: 0, + }, + segments: { + count: 4, + memory_in_bytes: 0, + terms_memory_in_bytes: 0, + stored_fields_memory_in_bytes: 0, + term_vectors_memory_in_bytes: 0, + norms_memory_in_bytes: 0, + points_memory_in_bytes: 0, + doc_values_memory_in_bytes: 0, + index_writer_memory_in_bytes: 0, + version_map_memory_in_bytes: 0, + fixed_bit_set_memory_in_bytes: 0, + max_unsafe_auto_id_timestamp: -1, + file_sizes: {}, + }, + translog: { + operations: 0, + size_in_bytes: 55, + uncommitted_operations: 0, + uncommitted_size_in_bytes: 55, + earliest_last_modified_age: 79289897, + }, + request_cache: { + memory_size_in_bytes: 3760, + evictions: 0, + hit_count: 20, + miss_count: 4, + }, + recovery: { + current_as_source: 0, + current_as_target: 0, + throttle_time_in_millis: 0, + }, + bulk: { + total_operations: 0, + total_time_in_millis: 0, + total_size_in_bytes: 0, + avg_time_in_millis: 0, + avg_size_in_bytes: 0, + }, + }, + }, }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/test_providers/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/test_providers/test_providers.tsx similarity index 100% rename from x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/test_providers/index.tsx rename to x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/test_providers/test_providers.tsx diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/unallowed_values/mock_unallowed_values.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/unallowed_values/mock_unallowed_values.ts new file mode 100644 index 0000000000000..393dd15ce9a20 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/mock/unallowed_values/mock_unallowed_values.ts @@ -0,0 +1,122 @@ +/* + * 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. + */ + +export const mockUnallowedValuesResponse = [ + { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 3, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + 'event.category': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'an_invalid_category', + doc_count: 2, + }, + { + key: 'theory', + doc_count: 1, + }, + ], + }, + }, + status: 200, + }, + { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 4, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + 'event.kind': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + }, + status: 200, + }, + { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 4, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + 'event.outcome': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + }, + status: 200, + }, + { + took: 0, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 4, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + 'event.type': { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [], + }, + }, + status: 200, + }, +]; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/styles.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/styles.tsx index d54ea9d6316e2..6fbf130d01b8f 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/styles.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/styles.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCode } from '@elastic/eui'; +import { EuiCode, EuiText } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import styled from 'styled-components'; @@ -21,3 +21,10 @@ export const CodeSuccess = styled(EuiCode)` export const CodeWarning = styled(EuiCode)` color: ${euiThemeVars.euiColorWarning}; `; + +export const FixedWidthLegendText = styled(EuiText)<{ + $width: number | undefined; +}>` + text-align: left; + ${({ $width }) => ($width != null ? `width: ${$width}px;` : '')} +`; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/translations.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/translations.ts index 771faa301cee3..53bedadd9361d 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/translations.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/translations.ts @@ -25,15 +25,6 @@ export const CHECKING = (index: string) => defaultMessage: 'Checking {index}', }); -export const COLLAPSE_BUTTON_LABEL = (collapsed: boolean) => - collapsed - ? i18n.translate('ecsDataQualityDashboard.collapseButtonLabelOpen', { - defaultMessage: 'Open', - }) - : i18n.translate('ecsDataQualityDashboard.collapseButtonLabelClosed', { - defaultMessage: 'Closed', - }); - export const COLD_DESCRIPTION = i18n.translate('ecsDataQualityDashboard.coldDescription', { defaultMessage: 'The index is no longer being updated and is queried infrequently. The information still needs to be searchable, but it’s okay if those queries are slower.', @@ -80,12 +71,6 @@ export const ECS_VERSION = i18n.translate('ecsDataQualityDashboard.ecsVersionSta defaultMessage: 'ECS version', }); -export const ERROR_LOADING_ECS_METADATA = (details: string) => - i18n.translate('ecsDataQualityDashboard.errorLoadingEcsMetadataLabel', { - values: { details }, - defaultMessage: 'Error loading ECS metadata: {details}', - }); - export const ERROR_LOADING_ECS_METADATA_TITLE = i18n.translate( 'ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingEcsMetadataTitle', { @@ -93,12 +78,6 @@ export const ERROR_LOADING_ECS_METADATA_TITLE = i18n.translate( } ); -export const ERROR_LOADING_ECS_VERSION = (details: string) => - i18n.translate('ecsDataQualityDashboard.errorLoadingEcsVersionLabel', { - values: { details }, - defaultMessage: 'Error loading ECS version: {details}', - }); - export const ERROR_LOADING_ECS_VERSION_TITLE = i18n.translate( 'ecsDataQualityDashboard.emptyErrorPrompt.errorLoadingEcsVersionTitle', { @@ -214,6 +193,10 @@ export const SELECT_ONE_OR_MORE_ILM_PHASES: string = i18n.translate( } ); +export const INDEX_SIZE_TOOLTIP = i18n.translate('ecsDataQualityDashboard.indexSizeTooltip', { + defaultMessage: 'The size of the primary index (does not include replicas)', +}); + export const TECHNICAL_PREVIEW = i18n.translate('ecsDataQualityDashboard.technicalPreviewBadge', { defaultMessage: 'Technical preview', }); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/types.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/types.ts index 9edc76bbe6220..d51e908bd7b38 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/types.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/types.ts @@ -10,6 +10,7 @@ import type { IndicesGetMappingIndexMappingRecord, IndicesStatsIndicesStats, } from '@elastic/elasticsearch/lib/api/types'; +import type { Direction } from '@elastic/eui'; export interface Mappings { pattern: string; @@ -113,6 +114,7 @@ export interface PatternRollup { indices: number | undefined; pattern: string; results: Record | undefined; + sizeInBytes: number | undefined; stats: Record | null; } @@ -139,6 +141,7 @@ export interface IndexToCheck { export type OnCheckCompleted = ({ error, + formatBytes, formatNumber, indexName, partitionedFieldMetadata, @@ -146,6 +149,7 @@ export type OnCheckCompleted = ({ version, }: { error: string | null; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata | null; @@ -158,3 +162,15 @@ export interface ErrorSummary { indexName: string | null; pattern: string; } + +export interface SortConfig { + sort: { + direction: Direction; + field: string; + }; +} + +export interface SelectedIndex { + indexName: string; + pattern: string; +} diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_mappings/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_mappings/helpers.test.ts new file mode 100644 index 0000000000000..e1865c31c85df --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_mappings/helpers.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { fetchMappings } from './helpers'; +import { mockMappingsResponse } from '../mock/mappings_response/mock_mappings_response'; + +describe('helpers', () => { + let originalFetch: typeof global['fetch']; + + beforeAll(() => { + originalFetch = global.fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + describe('fetchMappings', () => { + test('it returns the expected mappings', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockMappingsResponse), + }); + global.fetch = mockFetch; + + const result = await fetchMappings({ + abortController: new AbortController(), + patternOrIndexName: 'auditbeat-custom-index-1', + }); + + expect(result).toEqual({ + 'auditbeat-custom-index-1': { + mappings: { + properties: { + '@timestamp': { type: 'date' }, + event: { properties: { category: { ignore_above: 1024, type: 'keyword' } } }, + host: { + properties: { + name: { + fields: { keyword: { ignore_above: 256, type: 'keyword' } }, + type: 'text', + }, + }, + }, + some: { + properties: { + field: { + fields: { keyword: { ignore_above: 256, type: 'keyword' } }, + type: 'text', + }, + }, + }, + source: { + properties: { + ip: { fields: { keyword: { ignore_above: 256, type: 'keyword' } }, type: 'text' }, + port: { type: 'long' }, + }, + }, + }, + }, + }, + }); + }); + + test('it throws the expected error when fetch fails', async () => { + const error = 'simulated error'; + const mockFetch = jest.fn().mockResolvedValue({ + ok: false, + statusText: error, + }); + + global.fetch = mockFetch; + + await expect( + fetchMappings({ + abortController: new AbortController(), + patternOrIndexName: 'auditbeat-custom-index-1', + }) + ).rejects.toThrowError( + 'Error loading mappings for auditbeat-custom-index-1: simulated error' + ); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.test.ts new file mode 100644 index 0000000000000..f25903adff823 --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.test.ts @@ -0,0 +1,511 @@ +/* + * 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 numeral from '@elastic/numeral'; + +import { + getTotalDocsCount, + getTotalIncompatible, + getTotalIndices, + getTotalIndicesChecked, + onPatternRollupUpdated, + updateResultOnCheckCompleted, +} from './helpers'; +import { auditbeatWithAllResults } from '../mock/pattern_rollup/mock_auditbeat_pattern_rollup'; +import { + mockPacketbeatPatternRollup, + packetbeatNoResults, + packetbeatWithSomeErrors, +} from '../mock/pattern_rollup/mock_packetbeat_pattern_rollup'; +import { PatternRollup } from '../types'; +import { EMPTY_STAT } from '../helpers'; +import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types'; +import { mockPartitionedFieldMetadata } from '../mock/partitioned_field_metadata/mock_partitioned_field_metadata'; + +const defaultBytesFormat = '0,0.[0]b'; +const formatBytes = (value: number | undefined) => + value != null ? numeral(value).format(defaultBytesFormat) : EMPTY_STAT; + +const defaultNumberFormat = '0,0.[000]'; +const formatNumber = (value: number | undefined) => + value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT; + +const patternRollups: Record = { + 'auditbeat-*': auditbeatWithAllResults, // indices: 3 + 'packetbeat-*': mockPacketbeatPatternRollup, // indices: 2 +}; + +describe('helpers', () => { + let originalFetch: typeof global['fetch']; + + beforeAll(() => { + originalFetch = global.fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + describe('getTotalIndices', () => { + test('it returns the expected total when ALL `PatternRollup`s have an `indices`', () => { + expect(getTotalIndices(patternRollups)).toEqual(5); + }); + + test('it returns undefined when only SOME of the `PatternRollup`s have an `indices`', () => { + const someIndicesAreUndefined: Record = { + 'auditbeat-*': { + ...auditbeatWithAllResults, + indices: undefined, // <-- + }, + 'packetbeat-*': mockPacketbeatPatternRollup, // indices: 2 + }; + + expect(getTotalIndices(someIndicesAreUndefined)).toBeUndefined(); + }); + }); + + describe('getTotalDocsCount', () => { + test('it returns the expected total when ALL `PatternRollup`s have a `docsCount`', () => { + expect(getTotalDocsCount(patternRollups)).toEqual( + Number(auditbeatWithAllResults.docsCount) + Number(mockPacketbeatPatternRollup.docsCount) + ); + }); + + test('it returns undefined when only SOME of the `PatternRollup`s have a `docsCount`', () => { + const someIndicesAreUndefined: Record = { + 'auditbeat-*': { + ...auditbeatWithAllResults, + docsCount: undefined, // <-- + }, + 'packetbeat-*': mockPacketbeatPatternRollup, + }; + + expect(getTotalDocsCount(someIndicesAreUndefined)).toBeUndefined(); + }); + }); + + describe('getTotalIncompatible', () => { + test('it returns the expected total when ALL `PatternRollup`s have `results`', () => { + expect(getTotalIncompatible(patternRollups)).toEqual(4); + }); + + test('it returns the expected total when only SOME of the `PatternRollup`s have `results`', () => { + const someResultsAreUndefined: Record = { + 'auditbeat-*': auditbeatWithAllResults, + 'packetbeat-*': packetbeatNoResults, // <-- results is undefined + }; + + expect(getTotalIncompatible(someResultsAreUndefined)).toEqual(4); + }); + + test('it returns undefined when NONE of the `PatternRollup`s have `results`', () => { + const someResultsAreUndefined: Record = { + 'packetbeat-*': packetbeatNoResults, // <-- results is undefined + }; + + expect(getTotalIncompatible(someResultsAreUndefined)).toBeUndefined(); + }); + }); + + describe('getTotalIndicesChecked', () => { + test('it returns the expected total', () => { + expect(getTotalIndicesChecked(patternRollups)).toEqual(3); + }); + + test('it returns the expected total when errors have occurred', () => { + const someErrors: Record = { + 'auditbeat-*': auditbeatWithAllResults, // indices: 3 + 'packetbeat-*': packetbeatWithSomeErrors, // <-- indices: 2, but one has errors + }; + + expect(getTotalIndicesChecked(someErrors)).toEqual(4); + }); + }); + + describe('onPatternRollupUpdated', () => { + test('it returns a new collection with the updated rollup', () => { + const before: Record = { + 'auditbeat-*': auditbeatWithAllResults, + }; + + expect( + onPatternRollupUpdated({ + patternRollup: mockPacketbeatPatternRollup, + patternRollups: before, + }) + ).toEqual(patternRollups); + }); + }); + + describe('updateResultOnCheckCompleted', () => { + const packetbeatStats861: IndicesStatsIndicesStats = + mockPacketbeatPatternRollup.stats != null + ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.6.1-2023.02.04-000001'] + : {}; + const packetbeatStats853: IndicesStatsIndicesStats = + mockPacketbeatPatternRollup.stats != null + ? mockPacketbeatPatternRollup.stats['.ds-packetbeat-8.5.3-2023.02.04-000001'] + : {}; + + test('it returns the updated rollups', () => { + expect( + updateResultOnCheckCompleted({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': mockPacketbeatPatternRollup, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | `hot` | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + }, + }, + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, + }); + }); + + test('it returns the expected results when `patternRollup` does NOT have a `docsCount`', () => { + const noDocsCount = { + ...mockPacketbeatPatternRollup, + docsCount: undefined, // <-- + }; + + expect( + updateResultOnCheckCompleted({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': noDocsCount, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: undefined, // <-- + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 () | 3 | `hot` | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + }, + }, + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, + }); + }); + + test('it returns the expected results when `partitionedFieldMetadata` is null', () => { + expect( + updateResultOnCheckCompleted({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + partitionedFieldMetadata: null, // <-- + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': mockPacketbeatPatternRollup, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + index: '.ds-packetbeat-8.6.1-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536751379, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536751379, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536751809, + action: 'rollover', + action_time_millis: 1675536751809, + step: 'check-rollover-ready', + step_time_millis: 1675536751809, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + '.ds-packetbeat-8.5.3-2023.02.04-000001': { + index: '.ds-packetbeat-8.5.3-2023.02.04-000001', + managed: true, + policy: 'packetbeat', + index_creation_date_millis: 1675536774084, + time_since_index_creation: '25.26d', + lifecycle_date_millis: 1675536774084, + age: '25.26d', + phase: 'hot', + phase_time_millis: 1675536774416, + action: 'rollover', + action_time_millis: 1675536774416, + step: 'check-rollover-ready', + step_time_millis: 1675536774416, + phase_execution: { + policy: 'packetbeat', + version: 1, + modified_date_in_millis: 1675536751205, + }, + }, + }, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: 'hot', + incompatible: undefined, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [], + pattern: 'packetbeat-*', + }, + }, + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, + }); + }); + + test('it returns the updated rollups when there is no `partitionedFieldMetadata`', () => { + const noIlmExplain = { + ...mockPacketbeatPatternRollup, + ilmExplain: null, + }; + + expect( + updateResultOnCheckCompleted({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'packetbeat-*', + patternRollups: { + 'packetbeat-*': noIlmExplain, + }, + }) + ).toEqual({ + 'packetbeat-*': { + docsCount: 3258632, + error: null, + ilmExplain: null, + ilmExplainPhaseCounts: { + hot: 2, + warm: 0, + cold: 0, + frozen: 0, + unmanaged: 0, + }, + indices: 2, + pattern: 'packetbeat-*', + results: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': { + docsCount: 1628343, + error: null, + ilmPhase: undefined, + incompatible: 3, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + markdownComments: [ + '### .ds-packetbeat-8.6.1-2023.02.04-000001\n', + '| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | .ds-packetbeat-8.6.1-2023.02.04-000001 | 1,628,343 (50.0%) | 3 | -- | 697.7MB |\n\n', + '### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n', + "#### 3 incompatible fields, 0 fields with mappings in the same family\n\nFields are incompatible with ECS when index mappings, or the values of the fields in the index, don't conform to the Elastic Common Schema (ECS), version 8.6.1.\n\nIncompatible fields with mappings in the same family have exactly the same search behavior but may have different space usage or performance characteristics.\n\nWhen an incompatible field is not in the same family:\n❌ Detection engine rules referencing these fields may not match them correctly\n❌ Pages may not display some events or fields due to unexpected field mappings or values\n❌ Mappings or field values that don't comply with ECS are not supported\n", + '\n#### Incompatible field mappings - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS mapping type (expected) | Index mapping type (actual) | \n|-------|-----------------------------|-----------------------------|\n| host.name | `keyword` | `text` |\n| source.ip | `ip` | `text` |\n\n#### Incompatible field values - .ds-packetbeat-8.6.1-2023.02.04-000001\n\n\n| Field | ECS values (expected) | Document values (actual) | \n|-------|-----------------------|--------------------------|\n| event.category | `authentication`, `configuration`, `database`, `driver`, `email`, `file`, `host`, `iam`, `intrusion_detection`, `malware`, `network`, `package`, `process`, `registry`, `session`, `threat`, `vulnerability`, `web` | `an_invalid_category` (2), `theory` (1) |\n\n', + ], + pattern: 'packetbeat-*', + }, + }, + sizeInBytes: 1464758182, + stats: { + '.ds-packetbeat-8.6.1-2023.02.04-000001': packetbeatStats861, + '.ds-packetbeat-8.5.3-2023.02.04-000001': packetbeatStats853, + }, + }, + }); + }); + + test('it returns the unmodified rollups when `pattern` is not a member of `patternRollups`', () => { + const shouldNotBeModified: Record = { + 'packetbeat-*': mockPacketbeatPatternRollup, + }; + + expect( + updateResultOnCheckCompleted({ + error: null, + formatBytes, + formatNumber, + indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001', + partitionedFieldMetadata: mockPartitionedFieldMetadata, + pattern: 'this-pattern-is-not-in-pattern-rollups', // <-- + patternRollups: shouldNotBeModified, + }) + ).toEqual(shouldNotBeModified); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.ts index 995ac8eac86c4..dbad0364904f5 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/helpers.ts @@ -8,7 +8,11 @@ import { getIndexDocsCountFromRollup } from '../data_quality_panel/data_quality_summary/summary_actions/check_all/helpers'; import { getIlmPhase } from '../data_quality_panel/pattern/helpers'; import { getAllIncompatibleMarkdownComments } from '../data_quality_panel/tabs/incompatible_tab/helpers'; -import { getTotalPatternIncompatible, getTotalPatternIndicesChecked } from '../helpers'; +import { + getSizeInBytes, + getTotalPatternIncompatible, + getTotalPatternIndicesChecked, +} from '../helpers'; import type { IlmPhase, PartitionedFieldMetadata, PatternRollup } from '../types'; export const getTotalIndices = ( @@ -19,7 +23,7 @@ export const getTotalIndices = ( // only return the total when all `PatternRollup`s have a `indices`: return allRollupsHaveIndices - ? allRollups.reduce((acc, { indices }) => acc + (indices ?? 0), 0) + ? allRollups.reduce((acc, { indices }) => acc + Number(indices), 0) : undefined; }; @@ -31,7 +35,21 @@ export const getTotalDocsCount = ( // only return the total when all `PatternRollup`s have a `docsCount`: return allRollupsHaveDocsCount - ? allRollups.reduce((acc, { docsCount }) => acc + (docsCount ?? 0), 0) + ? allRollups.reduce((acc, { docsCount }) => acc + Number(docsCount), 0) + : undefined; +}; + +export const getTotalSizeInBytes = ( + patternRollups: Record +): number | undefined => { + const allRollups = Object.values(patternRollups); + const allRollupsHaveSizeInBytes = allRollups.every(({ sizeInBytes }) => + Number.isInteger(sizeInBytes) + ); + + // only return the total when all `PatternRollup`s have a `sizeInBytes`: + return allRollupsHaveSizeInBytes + ? allRollups.reduce((acc, { sizeInBytes }) => acc + Number(sizeInBytes), 0) : undefined; }; @@ -69,6 +87,7 @@ export const onPatternRollupUpdated = ({ export const updateResultOnCheckCompleted = ({ error, + formatBytes, formatNumber, indexName, partitionedFieldMetadata, @@ -76,6 +95,7 @@ export const updateResultOnCheckCompleted = ({ patternRollups, }: { error: string | null; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata | null; @@ -85,7 +105,7 @@ export const updateResultOnCheckCompleted = ({ const patternRollup: PatternRollup | undefined = patternRollups[pattern]; if (patternRollup != null) { - const ilmExplain = patternRollup.ilmExplain ?? null; + const ilmExplain = patternRollup.ilmExplain; const ilmPhase: IlmPhase | undefined = ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined; @@ -97,15 +117,19 @@ export const updateResultOnCheckCompleted = ({ const patternDocsCount = patternRollup.docsCount ?? 0; + const sizeInBytes = getSizeInBytes({ indexName, stats: patternRollup.stats }); + const markdownComments = partitionedFieldMetadata != null ? getAllIncompatibleMarkdownComments({ docsCount, + formatBytes, formatNumber, ilmPhase, indexName, partitionedFieldMetadata, patternDocsCount, + sizeInBytes, }) : []; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/index.tsx b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/index.tsx index da5745deaa3cd..1976b5e150de3 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/index.tsx +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_results_rollup/index.tsx @@ -17,6 +17,7 @@ import { getTotalIncompatible, getTotalIndices, getTotalIndicesChecked, + getTotalSizeInBytes, onPatternRollupUpdated, updateResultOnCheckCompleted, } from './helpers'; @@ -31,6 +32,7 @@ interface UseResultsRollup { totalIncompatible: number | undefined; totalIndices: number | undefined; totalIndicesChecked: number | undefined; + totalSizeInBytes: number | undefined; updatePatternIndexNames: ({ indexNames, pattern, @@ -58,6 +60,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll () => getTotalIndicesChecked(patternRollups), [patternRollups] ); + const totalSizeInBytes = useMemo(() => getTotalSizeInBytes(patternRollups), [patternRollups]); const updatePatternIndexNames = useCallback( ({ indexNames, pattern }: { indexNames: string[]; pattern: string }) => { @@ -72,12 +75,14 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll const onCheckCompleted: OnCheckCompleted = useCallback( ({ error, + formatBytes, formatNumber, indexName, partitionedFieldMetadata, pattern, }: { error: string | null; + formatBytes: (value: number | undefined) => string; formatNumber: (value: number | undefined) => string; indexName: string; partitionedFieldMetadata: PartitionedFieldMetadata | null; @@ -86,6 +91,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll setPatternRollups((current) => updateResultOnCheckCompleted({ error, + formatBytes, formatNumber, indexName, partitionedFieldMetadata, @@ -111,6 +117,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll totalIncompatible, totalIndices, totalIndicesChecked, + totalSizeInBytes, updatePatternIndexNames, updatePatternRollup, }; diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.test.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.test.ts new file mode 100644 index 0000000000000..2f80ba5e2cc7a --- /dev/null +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.test.ts @@ -0,0 +1,503 @@ +/* + * 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 { omit } from 'lodash/fp'; + +import { + fetchUnallowedValues, + getUnallowedValueCount, + getUnallowedValues, + isBucket, +} from './helpers'; +import { mockUnallowedValuesResponse } from '../mock/unallowed_values/mock_unallowed_values'; +import { UnallowedValueRequestItem, UnallowedValueSearchResult } from '../types'; + +describe('helpers', () => { + let originalFetch: typeof global['fetch']; + + beforeAll(() => { + originalFetch = global.fetch; + }); + + afterAll(() => { + global.fetch = originalFetch; + }); + + describe('isBucket', () => { + test('it returns true when the bucket is valid', () => { + expect( + isBucket({ + key: 'stop', + doc_count: 2, + }) + ).toBe(true); + }); + + test('it returns false when just `key` is missing', () => { + expect( + isBucket({ + doc_count: 2, + }) + ).toBe(false); + }); + + test('it returns false when just `key` has an incorrect type', () => { + expect( + isBucket({ + key: 1234, // <-- should be a string + doc_count: 2, + }) + ).toBe(false); + }); + + test('it returns false when just `doc_count` is missing', () => { + expect( + isBucket({ + key: 'stop', + }) + ).toBe(false); + }); + + test('it returns false when just `doc_count` has an incorrect type', () => { + expect( + isBucket({ + key: 'stop', + doc_count: 'foo', // <-- should be a number + }) + ).toBe(false); + }); + + test('it returns false when both `key` and `doc_count` are missing', () => { + expect(isBucket({})).toBe(false); + }); + + test('it returns false when both `key` and `doc_count` have incorrect types', () => { + expect( + isBucket({ + key: 1234, // <-- should be a string + doc_count: 'foo', // <-- should be a number + }) + ).toBe(false); + }); + + test('it returns false when `maybeBucket` is undefined', () => { + expect(isBucket(undefined)).toBe(false); + }); + }); + + describe('getUnallowedValueCount', () => { + test('it returns the expected count', () => { + expect( + getUnallowedValueCount({ + key: 'stop', + doc_count: 2, + }) + ).toEqual({ count: 2, fieldName: 'stop' }); + }); + }); + + describe('getUnallowedValues', () => { + const requestItems: UnallowedValueRequestItem[] = [ + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.category', + allowedValues: [ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.kind', + allowedValues: [ + 'alert', + 'enrichment', + 'event', + 'metric', + 'state', + 'pipeline_error', + 'signal', + ], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.outcome', + allowedValues: ['failure', 'success', 'unknown'], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.type', + allowedValues: [ + 'access', + 'admin', + 'allowed', + 'change', + 'connection', + 'creation', + 'deletion', + 'denied', + 'end', + 'error', + 'group', + 'indicator', + 'info', + 'installation', + 'protocol', + 'start', + 'user', + ], + }, + ]; + + const searchResults: UnallowedValueSearchResult[] = [ + { + aggregations: { + 'event.category': { + buckets: [ + { + key: 'an_invalid_category', + doc_count: 2, + }, + { + key: 'theory', + doc_count: 1, + }, + ], + }, + }, + }, + { + aggregations: { + 'event.kind': { + buckets: [], + }, + }, + }, + { + aggregations: { + 'event.outcome': { + buckets: [], + }, + }, + }, + { + aggregations: { + 'event.type': { + buckets: [], + }, + }, + }, + ]; + + test('it returns the expected unallowed values', () => { + expect( + getUnallowedValues({ + requestItems, + searchResults, + }) + ).toEqual({ + 'event.category': [ + { count: 2, fieldName: 'an_invalid_category' }, + { count: 1, fieldName: 'theory' }, + ], + 'event.kind': [], + 'event.outcome': [], + 'event.type': [], + }); + }); + + test('it returns an empty index when `searchResults` is null', () => { + expect( + getUnallowedValues({ + requestItems, + searchResults: null, + }) + ).toEqual({}); + }); + + test('it returns an empty index when `searchResults` is not an array', () => { + expect( + getUnallowedValues({ + requestItems, + // @ts-expect-error + searchResults: 1234, + }) + ).toEqual({}); + }); + + test('it returns the expected results when `searchResults` does NOT have `aggregations`', () => { + const noAggregations: UnallowedValueSearchResult[] = searchResults.map((x) => + omit('aggregations', x) + ); + + expect( + getUnallowedValues({ + requestItems, + searchResults: noAggregations, + }) + ).toEqual({ + 'event.category': [], + 'event.kind': [], + 'event.outcome': [], + 'event.type': [], + }); + }); + + test('it returns the expected unallowed values when SOME buckets are invalid', () => { + const someInvalid: UnallowedValueSearchResult[] = [ + { + aggregations: { + 'event.category': { + buckets: [ + { + key: 'foo', + // @ts-expect-error + doc_count: 'this-is-an-invalid-bucket', // <-- invalid type, should be number + }, + { + key: 'bar', + doc_count: 1, + }, + ], + }, + }, + }, + { + aggregations: { + 'event.kind': { + buckets: [], + }, + }, + }, + { + aggregations: { + 'event.outcome': { + buckets: [], + }, + }, + }, + { + aggregations: { + 'event.type': { + buckets: [], + }, + }, + }, + ]; + + expect( + getUnallowedValues({ + requestItems, + searchResults: someInvalid, + }) + ).toEqual({ + 'event.category': [{ count: 1, fieldName: 'bar' }], + 'event.kind': [], + 'event.outcome': [], + 'event.type': [], + }); + }); + }); + + describe('fetchUnallowedValues', () => { + const requestItems: UnallowedValueRequestItem[] = [ + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.category', + allowedValues: [ + 'authentication', + 'configuration', + 'database', + 'driver', + 'email', + 'file', + 'host', + 'iam', + 'intrusion_detection', + 'malware', + 'network', + 'package', + 'process', + 'registry', + 'session', + 'threat', + 'vulnerability', + 'web', + ], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.kind', + allowedValues: [ + 'alert', + 'enrichment', + 'event', + 'metric', + 'state', + 'pipeline_error', + 'signal', + ], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.outcome', + allowedValues: ['failure', 'success', 'unknown'], + }, + { + indexName: 'auditbeat-custom-index-1', + indexFieldName: 'event.type', + allowedValues: [ + 'access', + 'admin', + 'allowed', + 'change', + 'connection', + 'creation', + 'deletion', + 'denied', + 'end', + 'error', + 'group', + 'indicator', + 'info', + 'installation', + 'protocol', + 'start', + 'user', + ], + }, + ]; + + test('it includes the expected content in the `fetch` request', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockUnallowedValuesResponse), + }); + global.fetch = mockFetch; + const abortController = new AbortController(); + + await fetchUnallowedValues({ + abortController, + indexName: 'auditbeat-custom-index-1', + requestItems, + }); + + expect(mockFetch).toBeCalledWith( + '/internal/ecs_data_quality_dashboard/unallowed_field_values', + { + body: JSON.stringify(requestItems), + headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'xsrf' }, + method: 'POST', + signal: abortController.signal, + } + ); + }); + + test('it returns the expected unallowed values', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockUnallowedValuesResponse), + }); + global.fetch = mockFetch; + + const result = await fetchUnallowedValues({ + abortController: new AbortController(), + indexName: 'auditbeat-custom-index-1', + requestItems, + }); + + expect(result).toEqual([ + { + _shards: { failed: 0, skipped: 0, successful: 1, total: 1 }, + aggregations: { + 'event.category': { + buckets: [ + { doc_count: 2, key: 'an_invalid_category' }, + { doc_count: 1, key: 'theory' }, + ], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + }, + hits: { hits: [], max_score: null, total: { relation: 'eq', value: 3 } }, + status: 200, + timed_out: false, + took: 1, + }, + { + _shards: { failed: 0, skipped: 0, successful: 1, total: 1 }, + aggregations: { + 'event.kind': { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, + }, + hits: { hits: [], max_score: null, total: { relation: 'eq', value: 4 } }, + status: 200, + timed_out: false, + took: 0, + }, + { + _shards: { failed: 0, skipped: 0, successful: 1, total: 1 }, + aggregations: { + 'event.outcome': { + buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + }, + }, + hits: { hits: [], max_score: null, total: { relation: 'eq', value: 4 } }, + status: 200, + timed_out: false, + took: 0, + }, + { + _shards: { failed: 0, skipped: 0, successful: 1, total: 1 }, + aggregations: { + 'event.type': { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, + }, + hits: { hits: [], max_score: null, total: { relation: 'eq', value: 4 } }, + status: 200, + timed_out: false, + took: 0, + }, + ]); + }); + + test('it throws the expected error when fetch fails', async () => { + const error = 'simulated error'; + const mockFetch = jest.fn().mockResolvedValue({ + ok: false, + statusText: error, + }); + global.fetch = mockFetch; + + await expect( + fetchUnallowedValues({ + abortController: new AbortController(), + indexName: 'auditbeat-custom-index-1', + requestItems, + }) + ).rejects.toThrowError( + 'Error loading unallowed values for index auditbeat-custom-index-1: simulated error' + ); + }); + }); +}); diff --git a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.ts b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.ts index 1fa8991ce9264..e1ee93b72b283 100644 --- a/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.ts +++ b/x-pack/packages/kbn-ecs-data-quality-dashboard/impl/data_quality/use_unallowed_values/helpers.ts @@ -16,6 +16,7 @@ import type { const UNALLOWED_VALUES_API_ROUTE = '/internal/ecs_data_quality_dashboard/unallowed_field_values'; export const isBucket = (maybeBucket: unknown): maybeBucket is Bucket => + maybeBucket != null && typeof (maybeBucket as Bucket).key === 'string' && typeof (maybeBucket as Bucket).doc_count === 'number'; diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index 2d75eee57bece..07cc2a491cf02 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -103,7 +103,6 @@ export const ecsDataQualityDashboardLinks: LinkItem = { ), path: DATA_QUALITY_PATH, capabilities: [`${SERVER_APP_ID}.show`], - isBeta: true, globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.ecsDataQualityDashboard', { defaultMessage: 'Data Quality', diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index 35a991e6ae7ad..4f7df91715247 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -33,11 +33,10 @@ import { SecurityPageName } from '../../app/types'; import { getGroupByFieldsOnClick } from '../../common/components/alerts_treemap/lib/helpers'; import { useTheme } from '../../common/components/charts/common'; import { HeaderPage } from '../../common/components/header_page'; -import type { BadgeOptions } from '../../common/components/header_page/types'; import { LandingPageComponent } from '../../common/components/landing_page'; import { useLocalStorage } from '../../common/components/local_storage'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useGetUserCasesPermissions, @@ -51,11 +50,6 @@ import * as i18n from './translations'; const LOCAL_STORAGE_KEY = 'dataQualityDashboardLastChecked'; -const badgeOptions: BadgeOptions = { - beta: true, - text: i18n.BETA, -}; - const comboBoxStyle: React.CSSProperties = { width: '322px', }; @@ -141,6 +135,7 @@ const DataQualityComponent: React.FC = () => { }, [toasts] ); + const [defaultBytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const labelInputId = useGeneratedHtmlId({ prefix: 'labelInput' }); const [selectedOptions, setSelectedOptions] = useState(defaultOptions); @@ -210,11 +205,7 @@ const DataQualityComponent: React.FC = () => { {indicesExist ? ( <> - + { Date: Tue, 25 Apr 2023 00:58:48 -0400 Subject: [PATCH 33/36] [api-docs] 2023-04-25 Daily api_docs build (#155679) Generated by https://buildkite.com/elastic/kibana-api-docs-daily/builds/318 --- api_docs/actions.mdx | 2 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.devdocs.json | 53 + api_docs/alerting.mdx | 4 +- api_docs/apm.mdx | 2 +- api_docs/asset_manager.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.devdocs.json | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.mdx | 2 +- api_docs/cloud_chat.mdx | 2 +- api_docs/cloud_data_migration.mdx | 2 +- api_docs/cloud_defend.mdx | 2 +- api_docs/cloud_experiments.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/content_management.mdx | 2 +- api_docs/controls.devdocs.json | 11 +- api_docs/controls.mdx | 2 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.devdocs.json | 4 +- api_docs/data.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.devdocs.json | 6 +- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 8 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/ecs_data_quality_dashboard.mdx | 2 +- api_docs/embeddable.devdocs.json | 34 +- api_docs/embeddable.mdx | 4 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.devdocs.json | 6 +- api_docs/event_log.mdx | 2 +- api_docs/exploratory_view.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.mdx | 2 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_legacy_metric_vis.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/files.devdocs.json | 62 +- api_docs/files.mdx | 4 +- api_docs/files_management.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/guided_onboarding.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/image_embeddable.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.devdocs.json | 14 + api_docs/infra.mdx | 4 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_components.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerting_state_types.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_alerts_as_data_utils.mdx | 2 +- api_docs/kbn_alerts_ui_shared.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_analytics_shippers_gainsight.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_synthtrace.mdx | 2 +- api_docs/kbn_apm_synthtrace_client.mdx | 2 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_cases_components.mdx | 2 +- api_docs/kbn_cell_actions.mdx | 2 +- api_docs/kbn_chart_expressions_common.mdx | 2 +- api_docs/kbn_chart_icons.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- api_docs/kbn_ci_stats_performance_metrics.mdx | 2 +- api_docs/kbn_ci_stats_reporter.mdx | 2 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_code_editor.mdx | 2 +- api_docs/kbn_code_editor_mocks.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- .../kbn_content_management_content_editor.mdx | 2 +- ...content_management_table_list.devdocs.json | 20 +- .../kbn_content_management_table_list.mdx | 7 +- .../kbn_content_management_utils.devdocs.json | 1201 +++++++++++++++++ api_docs/kbn_content_management_utils.mdx | 33 + api_docs/kbn_core_analytics_browser.mdx | 2 +- .../kbn_core_analytics_browser_internal.mdx | 2 +- api_docs/kbn_core_analytics_browser_mocks.mdx | 2 +- api_docs/kbn_core_analytics_server.mdx | 2 +- .../kbn_core_analytics_server_internal.mdx | 2 +- api_docs/kbn_core_analytics_server_mocks.mdx | 2 +- api_docs/kbn_core_application_browser.mdx | 2 +- .../kbn_core_application_browser_internal.mdx | 2 +- .../kbn_core_application_browser_mocks.mdx | 2 +- api_docs/kbn_core_application_common.mdx | 2 +- api_docs/kbn_core_apps_browser_internal.mdx | 2 +- api_docs/kbn_core_apps_browser_mocks.mdx | 2 +- api_docs/kbn_core_apps_server_internal.mdx | 2 +- api_docs/kbn_core_base_browser_mocks.mdx | 2 +- api_docs/kbn_core_base_common.mdx | 2 +- api_docs/kbn_core_base_server_internal.mdx | 2 +- api_docs/kbn_core_base_server_mocks.mdx | 2 +- .../kbn_core_capabilities_browser_mocks.mdx | 2 +- api_docs/kbn_core_capabilities_common.mdx | 2 +- api_docs/kbn_core_capabilities_server.mdx | 2 +- .../kbn_core_capabilities_server_mocks.mdx | 2 +- api_docs/kbn_core_chrome_browser.mdx | 2 +- api_docs/kbn_core_chrome_browser_mocks.mdx | 2 +- api_docs/kbn_core_config_server_internal.mdx | 2 +- api_docs/kbn_core_custom_branding_browser.mdx | 2 +- ..._core_custom_branding_browser_internal.mdx | 2 +- ...kbn_core_custom_branding_browser_mocks.mdx | 2 +- api_docs/kbn_core_custom_branding_common.mdx | 2 +- api_docs/kbn_core_custom_branding_server.mdx | 2 +- ...n_core_custom_branding_server_internal.mdx | 2 +- .../kbn_core_custom_branding_server_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_browser.mdx | 2 +- ...kbn_core_deprecations_browser_internal.mdx | 2 +- .../kbn_core_deprecations_browser_mocks.mdx | 2 +- api_docs/kbn_core_deprecations_common.mdx | 2 +- api_docs/kbn_core_deprecations_server.mdx | 2 +- .../kbn_core_deprecations_server_internal.mdx | 2 +- .../kbn_core_deprecations_server_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- ...e_elasticsearch_client_server_internal.mdx | 2 +- ...core_elasticsearch_client_server_mocks.mdx | 2 +- api_docs/kbn_core_elasticsearch_server.mdx | 2 +- ...kbn_core_elasticsearch_server_internal.mdx | 2 +- .../kbn_core_elasticsearch_server_mocks.mdx | 2 +- .../kbn_core_environment_server_internal.mdx | 2 +- .../kbn_core_environment_server_mocks.mdx | 2 +- .../kbn_core_execution_context_browser.mdx | 2 +- ...ore_execution_context_browser_internal.mdx | 2 +- ...n_core_execution_context_browser_mocks.mdx | 2 +- .../kbn_core_execution_context_common.mdx | 2 +- .../kbn_core_execution_context_server.mdx | 2 +- ...core_execution_context_server_internal.mdx | 2 +- ...bn_core_execution_context_server_mocks.mdx | 2 +- api_docs/kbn_core_fatal_errors_browser.mdx | 2 +- .../kbn_core_fatal_errors_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_browser.mdx | 2 +- api_docs/kbn_core_http_browser_internal.mdx | 2 +- api_docs/kbn_core_http_browser_mocks.mdx | 2 +- api_docs/kbn_core_http_common.mdx | 2 +- .../kbn_core_http_context_server_mocks.mdx | 2 +- ...re_http_request_handler_context_server.mdx | 2 +- api_docs/kbn_core_http_resources_server.mdx | 2 +- ...bn_core_http_resources_server_internal.mdx | 2 +- .../kbn_core_http_resources_server_mocks.mdx | 2 +- .../kbn_core_http_router_server_internal.mdx | 2 +- .../kbn_core_http_router_server_mocks.mdx | 2 +- api_docs/kbn_core_http_server.mdx | 2 +- api_docs/kbn_core_http_server_internal.mdx | 2 +- api_docs/kbn_core_http_server_mocks.mdx | 2 +- api_docs/kbn_core_i18n_browser.mdx | 2 +- api_docs/kbn_core_i18n_browser_mocks.mdx | 2 +- api_docs/kbn_core_i18n_server.mdx | 2 +- api_docs/kbn_core_i18n_server_internal.mdx | 2 +- api_docs/kbn_core_i18n_server_mocks.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- ...kbn_core_integrations_browser_internal.mdx | 2 +- .../kbn_core_integrations_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_browser.mdx | 2 +- api_docs/kbn_core_lifecycle_browser_mocks.mdx | 2 +- api_docs/kbn_core_lifecycle_server.mdx | 2 +- api_docs/kbn_core_lifecycle_server_mocks.mdx | 2 +- api_docs/kbn_core_logging_browser_mocks.mdx | 2 +- api_docs/kbn_core_logging_common_internal.mdx | 2 +- api_docs/kbn_core_logging_server.mdx | 2 +- api_docs/kbn_core_logging_server_internal.mdx | 2 +- api_docs/kbn_core_logging_server_mocks.mdx | 2 +- ...ore_metrics_collectors_server_internal.mdx | 2 +- ...n_core_metrics_collectors_server_mocks.mdx | 2 +- api_docs/kbn_core_metrics_server.mdx | 2 +- api_docs/kbn_core_metrics_server_internal.mdx | 2 +- api_docs/kbn_core_metrics_server_mocks.mdx | 2 +- api_docs/kbn_core_mount_utils_browser.mdx | 2 +- api_docs/kbn_core_node_server.mdx | 2 +- api_docs/kbn_core_node_server_internal.mdx | 2 +- api_docs/kbn_core_node_server_mocks.mdx | 2 +- api_docs/kbn_core_notifications_browser.mdx | 2 +- ...bn_core_notifications_browser_internal.mdx | 2 +- .../kbn_core_notifications_browser_mocks.mdx | 2 +- api_docs/kbn_core_overlays_browser.mdx | 2 +- .../kbn_core_overlays_browser_internal.mdx | 2 +- api_docs/kbn_core_overlays_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_browser.mdx | 2 +- api_docs/kbn_core_plugins_browser_mocks.mdx | 2 +- api_docs/kbn_core_plugins_server.mdx | 2 +- api_docs/kbn_core_plugins_server_mocks.mdx | 2 +- api_docs/kbn_core_preboot_server.mdx | 2 +- api_docs/kbn_core_preboot_server_mocks.mdx | 2 +- api_docs/kbn_core_rendering_browser_mocks.mdx | 2 +- .../kbn_core_rendering_server_internal.mdx | 2 +- api_docs/kbn_core_rendering_server_mocks.mdx | 2 +- api_docs/kbn_core_root_server_internal.mdx | 2 +- .../kbn_core_saved_objects_api_browser.mdx | 2 +- .../kbn_core_saved_objects_api_server.mdx | 2 +- ...core_saved_objects_api_server_internal.mdx | 2 +- ...bn_core_saved_objects_api_server_mocks.mdx | 2 +- ...ore_saved_objects_base_server_internal.mdx | 2 +- ...n_core_saved_objects_base_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_browser.mdx | 2 +- ...bn_core_saved_objects_browser_internal.mdx | 2 +- .../kbn_core_saved_objects_browser_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_common.mdx | 2 +- ..._objects_import_export_server_internal.mdx | 2 +- ...ved_objects_import_export_server_mocks.mdx | 2 +- ...aved_objects_migration_server_internal.mdx | 2 +- ...e_saved_objects_migration_server_mocks.mdx | 2 +- api_docs/kbn_core_saved_objects_server.mdx | 2 +- ...kbn_core_saved_objects_server_internal.mdx | 2 +- .../kbn_core_saved_objects_server_mocks.mdx | 2 +- .../kbn_core_saved_objects_utils_server.mdx | 2 +- api_docs/kbn_core_status_common.mdx | 2 +- api_docs/kbn_core_status_common_internal.mdx | 2 +- api_docs/kbn_core_status_server.mdx | 2 +- api_docs/kbn_core_status_server_internal.mdx | 2 +- api_docs/kbn_core_status_server_mocks.mdx | 2 +- ...core_test_helpers_deprecations_getters.mdx | 2 +- ...n_core_test_helpers_http_setup_browser.mdx | 2 +- api_docs/kbn_core_test_helpers_kbn_server.mdx | 2 +- ...n_core_test_helpers_so_type_serializer.mdx | 2 +- api_docs/kbn_core_test_helpers_test_utils.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_internal.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_browser.mdx | 2 +- .../kbn_core_ui_settings_browser_internal.mdx | 2 +- .../kbn_core_ui_settings_browser_mocks.mdx | 2 +- api_docs/kbn_core_ui_settings_common.mdx | 2 +- api_docs/kbn_core_ui_settings_server.mdx | 2 +- .../kbn_core_ui_settings_server_internal.mdx | 2 +- .../kbn_core_ui_settings_server_mocks.mdx | 2 +- api_docs/kbn_core_usage_data_server.mdx | 2 +- .../kbn_core_usage_data_server_internal.mdx | 2 +- api_docs/kbn_core_usage_data_server_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_crypto_browser.mdx | 2 +- api_docs/kbn_cypress_config.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.devdocs.json | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_dom_drag_drop.mdx | 2 +- api_docs/kbn_ebt_tools.mdx | 2 +- api_docs/kbn_ecs.mdx | 2 +- api_docs/kbn_ecs_data_quality_dashboard.mdx | 2 +- api_docs/kbn_es.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_errors.mdx | 2 +- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_es_types.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_expandable_flyout.mdx | 2 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- .../kbn_ftr_common_functional_services.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_generate_csv.mdx | 2 +- api_docs/kbn_generate_csv_types.mdx | 2 +- api_docs/kbn_guided_onboarding.mdx | 2 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_hapi_mocks.mdx | 2 +- api_docs/kbn_health_gateway_server.mdx | 2 +- api_docs/kbn_home_sample_data_card.mdx | 2 +- api_docs/kbn_home_sample_data_tab.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_i18n_react.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.devdocs.json | 25 + api_docs/kbn_io_ts_utils.mdx | 4 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_journeys.mdx | 2 +- api_docs/kbn_json_ast.mdx | 2 +- api_docs/kbn_kibana_manifest_schema.mdx | 2 +- .../kbn_language_documentation_popover.mdx | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_managed_vscode_config.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_ml_agg_utils.mdx | 2 +- api_docs/kbn_ml_date_picker.mdx | 2 +- api_docs/kbn_ml_error_utils.mdx | 2 +- api_docs/kbn_ml_is_defined.mdx | 2 +- api_docs/kbn_ml_is_populated_object.mdx | 2 +- api_docs/kbn_ml_local_storage.mdx | 2 +- api_docs/kbn_ml_nested_property.mdx | 2 +- api_docs/kbn_ml_number_utils.mdx | 2 +- api_docs/kbn_ml_query_utils.mdx | 2 +- api_docs/kbn_ml_random_sampler_utils.mdx | 2 +- api_docs/kbn_ml_route_utils.mdx | 2 +- api_docs/kbn_ml_string_hash.mdx | 2 +- api_docs/kbn_ml_trained_models_utils.mdx | 2 +- api_docs/kbn_ml_url_state.mdx | 2 +- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_object_versioning.mdx | 2 +- api_docs/kbn_observability_alert_details.mdx | 2 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- api_docs/kbn_osquery_io_ts_types.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_repo_file_maps.mdx | 2 +- api_docs/kbn_repo_linter.mdx | 2 +- api_docs/kbn_repo_path.mdx | 2 +- api_docs/kbn_repo_source_classifier.mdx | 2 +- api_docs/kbn_reporting_common.mdx | 2 +- api_docs/kbn_rison.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- api_docs/kbn_saved_objects_settings.mdx | 2 +- api_docs/kbn_security_solution_side_nav.mdx | 2 +- ...kbn_security_solution_storybook_config.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_data_table.mdx | 2 +- api_docs/kbn_securitysolution_ecs.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- ...ritysolution_exception_list_components.mdx | 2 +- ...kbn_securitysolution_grouping.devdocs.json | 150 +- api_docs/kbn_securitysolution_grouping.mdx | 4 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ..._securitysolution_io_ts_alerting_types.mdx | 2 +- .../kbn_securitysolution_io_ts_list_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- api_docs/kbn_securitysolution_list_api.mdx | 2 +- .../kbn_securitysolution_list_constants.mdx | 2 +- api_docs/kbn_securitysolution_list_hooks.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_svg.mdx | 2 +- api_docs/kbn_shared_ux_avatar_solution.mdx | 2 +- ...ared_ux_avatar_user_profile_components.mdx | 2 +- .../kbn_shared_ux_button_exit_full_screen.mdx | 2 +- ...hared_ux_button_exit_full_screen_mocks.mdx | 2 +- api_docs/kbn_shared_ux_button_toolbar.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data.mdx | 2 +- api_docs/kbn_shared_ux_card_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_context.mdx | 2 +- api_docs/kbn_shared_ux_file_image.mdx | 2 +- api_docs/kbn_shared_ux_file_image_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_mocks.mdx | 2 +- api_docs/kbn_shared_ux_file_picker.mdx | 2 +- .../kbn_shared_ux_file_types.devdocs.json | 20 +- api_docs/kbn_shared_ux_file_types.mdx | 4 +- api_docs/kbn_shared_ux_file_upload.mdx | 2 +- api_docs/kbn_shared_ux_file_util.mdx | 2 +- api_docs/kbn_shared_ux_link_redirect_app.mdx | 2 +- .../kbn_shared_ux_link_redirect_app_mocks.mdx | 2 +- api_docs/kbn_shared_ux_markdown.mdx | 2 +- api_docs/kbn_shared_ux_markdown_mocks.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- ...shared_ux_page_analytics_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- ...bn_shared_ux_page_kibana_no_data_mocks.mdx | 2 +- .../kbn_shared_ux_page_kibana_template.mdx | 2 +- ...n_shared_ux_page_kibana_template_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data.mdx | 2 +- .../kbn_shared_ux_page_no_data_config.mdx | 2 +- ...bn_shared_ux_page_no_data_config_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_no_data_mocks.mdx | 2 +- api_docs/kbn_shared_ux_page_solution_nav.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- ...n_shared_ux_prompt_no_data_views_mocks.mdx | 2 +- api_docs/kbn_shared_ux_prompt_not_found.mdx | 2 +- api_docs/kbn_shared_ux_router.mdx | 2 +- api_docs/kbn_shared_ux_router_mocks.mdx | 2 +- api_docs/kbn_shared_ux_storybook_config.mdx | 2 +- api_docs/kbn_shared_ux_storybook_mock.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_slo_schema.mdx | 2 +- api_docs/kbn_some_dev_log.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.mdx | 2 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_test_subj_selector.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_ts_projects.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_actions_browser.mdx | 2 +- api_docs/kbn_ui_shared_deps_src.mdx | 2 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_url_state.mdx | 2 +- api_docs/kbn_user_profile_components.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kbn_yarn_lock_validator.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.mdx | 2 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.mdx | 2 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/notifications.mdx | 2 +- api_docs/observability.mdx | 2 +- api_docs/observability_shared.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 23 +- api_docs/presentation_util.mdx | 2 +- api_docs/profiling.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_finder.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- api_docs/saved_objects_tagging_oss.mdx | 2 +- api_docs/saved_search.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.mdx | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/stack_connectors.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/threat_intelligence.mdx | 2 +- api_docs/timelines.devdocs.json | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.mdx | 2 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_field_list.mdx | 2 +- api_docs/unified_histogram.mdx | 2 +- api_docs/unified_search.mdx | 2 +- api_docs/unified_search_autocomplete.mdx | 2 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 517 files changed, 2051 insertions(+), 640 deletions(-) create mode 100644 api_docs/kbn_content_management_utils.devdocs.json create mode 100644 api_docs/kbn_content_management_utils.mdx diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 9f6783794ad2c..9c6b8a0a1c6a5 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github description: API docs for the actions plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] --- import actionsObj from './actions.devdocs.json'; diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index f41313a90546b..bfad5ad4db058 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github description: API docs for the advancedSettings plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] --- import advancedSettingsObj from './advanced_settings.devdocs.json'; diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 50cb3ba36668e..e772deb88951a 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github description: API docs for the aiops plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] --- import aiopsObj from './aiops.devdocs.json'; diff --git a/api_docs/alerting.devdocs.json b/api_docs/alerting.devdocs.json index 264f3c10d43f3..81ad124973bc6 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -9313,6 +9313,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-common.INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH", + "type": "string", + "tags": [], + "label": "INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH", + "description": [], + "signature": [ + "\"/internal/alerting/rules/maintenance_window/_active\"" + ], + "path": "x-pack/plugins/alerting/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH", + "type": "string", + "tags": [], + "label": "INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH", + "description": [], + "signature": [ + "\"/internal/alerting/rules/maintenance_window\"" + ], + "path": "x-pack/plugins/alerting/common/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-common.INTERNAL_BASE_ALERTING_API_PATH", @@ -9441,6 +9471,29 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "alerting", + "id": "def-common.MaintenanceWindowCreateBody", + "type": "Type", + "tags": [], + "label": "MaintenanceWindowCreateBody", + "description": [], + "signature": [ + "{ title: string; duration: number; rRule: ", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.RRuleParams", + "text": "RRuleParams" + }, + "; }" + ], + "path": "x-pack/plugins/alerting/common/maintenance_window.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "alerting", "id": "def-common.MaintenanceWindowSOAttributes", diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 05cd2479d10f4..da7722790ce0e 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github description: API docs for the alerting plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 606 | 1 | 585 | 42 | +| 609 | 1 | 588 | 42 | ## Client diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index 39984c4c85e92..2bb04a1e4c960 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github description: API docs for the apm plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/asset_manager.mdx b/api_docs/asset_manager.mdx index 3fdc2e696e3f0..428201f0e8998 100644 --- a/api_docs/asset_manager.mdx +++ b/api_docs/asset_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/assetManager title: "assetManager" image: https://source.unsplash.com/400x175/?github description: API docs for the assetManager plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'assetManager'] --- import assetManagerObj from './asset_manager.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index f79d12ad047ef..6ea84c4870d6d 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github description: API docs for the banners plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] --- import bannersObj from './banners.devdocs.json'; diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index fc6dbe517b369..e597e32169fde 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github description: API docs for the bfetch plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] --- import bfetchObj from './bfetch.devdocs.json'; diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index bbeae3846322f..e4438fdd61abe 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github description: API docs for the canvas plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] --- import canvasObj from './canvas.devdocs.json'; diff --git a/api_docs/cases.devdocs.json b/api_docs/cases.devdocs.json index f41f5e5efe59a..ce453c5849ecf 100644 --- a/api_docs/cases.devdocs.json +++ b/api_docs/cases.devdocs.json @@ -898,7 +898,7 @@ "CaseSeverity", " | undefined; assignees?: string | string[] | undefined; reporters?: string | string[] | undefined; defaultSearchOperator?: \"AND\" | \"OR\" | undefined; fields?: string | string[] | undefined; from?: string | undefined; page?: number | undefined; perPage?: number | undefined; search?: string | undefined; searchFields?: string | string[] | undefined; rootSearchFields?: string[] | undefined; sortField?: string | undefined; sortOrder?: \"asc\" | \"desc\" | undefined; to?: string | undefined; owner?: string | string[] | undefined; }, signal?: AbortSignal | undefined) => Promise<", "Cases", - ">; getCasesStatus: (query: { from?: string | undefined; to?: string | undefined; owner?: string | string[] | undefined; }, signal?: AbortSignal | undefined) => Promise<{ countOpenCases: number; countInProgressCases: number; countClosedCases: number; }>; getCasesMetrics: (query: { features: string[]; } & { from?: string | undefined; to?: string | undefined; owner?: string | string[] | undefined; }, signal?: AbortSignal | undefined) => Promise<{ mttr?: number | null | undefined; }>; bulkGet: (params: ", + ">; getCasesStatus: (query: { from?: string | undefined; to?: string | undefined; owner?: string | string[] | undefined; }, signal?: AbortSignal | undefined) => Promise<{ countOpenCases: number; countInProgressCases: number; countClosedCases: number; }>; getCasesMetrics: (query: { features: string[]; } & { from?: string | undefined; to?: string | undefined; owner?: string | string[] | undefined; }, signal?: AbortSignal | undefined) => Promise<{ mttr?: number | null | undefined; }>; bulkGet: (params: ", { "pluginId": "cases", "scope": "common", diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 15ec086b8eaf2..d5913a5ac4189 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github description: API docs for the cases plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] --- import casesObj from './cases.devdocs.json'; diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 8a55c2821385f..cc0458a58cf89 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github description: API docs for the charts plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] --- import chartsObj from './charts.devdocs.json'; diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index c09cbda53a15c..c8a0f4283501f 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github description: API docs for the cloud plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 53a1a634088eb..8e17fcb273582 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index cf1dca265856b..21fc2533c7989 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index 815fae9c2fb42..88b3059b376bf 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index c56ad03988470..eb6d552316f32 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 7b95ed904f226..2869a4d8b827d 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudSecurityPosture plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] --- import cloudSecurityPostureObj from './cloud_security_posture.devdocs.json'; diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 2f84a61fdae2e..0f4d045b8511e 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github description: API docs for the console plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index 21f71777a11e1..c885da1946899 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; diff --git a/api_docs/controls.devdocs.json b/api_docs/controls.devdocs.json index 42f533bf0fddc..ce95034b22ea9 100644 --- a/api_docs/controls.devdocs.json +++ b/api_docs/controls.devdocs.json @@ -772,7 +772,7 @@ "section": "def-public.ControlGroupContainer", "text": "ControlGroupContainer" }, - ", controlInputTransform?: ", + ", options?: { controlInputTransform?: ", { "pluginId": "controls", "scope": "common", @@ -780,7 +780,7 @@ "section": "def-common.ControlInputTransform", "text": "ControlInputTransform" }, - " | undefined) => void" + " | undefined; onSave?: ((id: string) => void) | undefined; } | undefined) => void" ], "path": "src/plugins/controls/public/control_group/embeddable/control_group_container.tsx", "deprecated": false, @@ -790,11 +790,12 @@ { "parentPluginId": "controls", "id": "def-public.ControlGroupContainer.openAddDataControlFlyout.$1", - "type": "Function", + "type": "Object", "tags": [], - "label": "controlInputTransform", + "label": "options", "description": [], "signature": [ + "{ controlInputTransform?: ", { "pluginId": "controls", "scope": "common", @@ -802,7 +803,7 @@ "section": "def-common.ControlInputTransform", "text": "ControlInputTransform" }, - " | undefined" + " | undefined; onSave?: ((id: string) => void) | undefined; } | undefined" ], "path": "src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx", "deprecated": false, diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index c280197a6417a..7ee1e24ae78ab 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github description: API docs for the controls plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 53316962496a5..ee60cc5194c9a 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github description: API docs for the customIntegrations plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] --- import customIntegrationsObj from './custom_integrations.devdocs.json'; diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index ee4d46dc9145f..ff02cd4aa5a3c 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboard plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] --- import dashboardObj from './dashboard.devdocs.json'; diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index e79d7c3abd944..35930f4037257 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the dashboardEnhanced plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] --- import dashboardEnhancedObj from './dashboard_enhanced.devdocs.json'; diff --git a/api_docs/data.devdocs.json b/api_docs/data.devdocs.json index a1ab4045a706c..a5f45916bbd6d 100644 --- a/api_docs/data.devdocs.json +++ b/api_docs/data.devdocs.json @@ -13543,7 +13543,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx" + "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx" }, { "plugin": "securitySolution", @@ -21181,7 +21181,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx" + "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx" }, { "plugin": "securitySolution", diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f58d4af3b6e61..034d48ef5dd4f 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github description: API docs for the data plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] --- import dataObj from './data.devdocs.json'; diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index 8fd1bc5eca79f..44c7b6a806f88 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github description: API docs for the data.query plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] --- import dataQueryObj from './data_query.devdocs.json'; diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index f56c297a354ba..5618420b30094 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github description: API docs for the data.search plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] --- import dataSearchObj from './data_search.devdocs.json'; diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 776cf231e1e62..1d3df0cb9f4b1 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewEditor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] --- import dataViewEditorObj from './data_view_editor.devdocs.json'; diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 4ac05486048f3..316058cdc98df 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewFieldEditor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] --- import dataViewFieldEditorObj from './data_view_field_editor.devdocs.json'; diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index 1e27fc11d34a9..742c53d11f637 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViewManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] --- import dataViewManagementObj from './data_view_management.devdocs.json'; diff --git a/api_docs/data_views.devdocs.json b/api_docs/data_views.devdocs.json index 5dab9a07d047d..dd2c5312568f6 100644 --- a/api_docs/data_views.devdocs.json +++ b/api_docs/data_views.devdocs.json @@ -85,7 +85,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx" + "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx" }, { "plugin": "securitySolution", @@ -8350,7 +8350,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx" + "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx" }, { "plugin": "securitySolution", @@ -15672,7 +15672,7 @@ }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx" + "path": "x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx" }, { "plugin": "securitySolution", diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 57c1558717fa7..1ece2df53bcd5 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github description: API docs for the dataViews plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] --- import dataViewsObj from './data_views.devdocs.json'; diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 1a34b399f959c..9a015b1c8a057 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github description: API docs for the dataVisualizer plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] --- import dataVisualizerObj from './data_visualizer.devdocs.json'; diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index b98c2fea8fc27..5249f3b7fb8aa 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index b49d0197cbea0..06e3460f2f31f 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin description: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -1139,12 +1139,12 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/ | | [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject) | - | | | [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | | | [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [user_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/user_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject), [host_risk_score_dashboards.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/risk_score/prebuilt_saved_objects/saved_object/host_risk_score_dashboards.ts#:~:text=SavedObject)+ 2 more | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 20 more | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_sub_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 20 more | - | | | [wrap_search_source_client.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.ts#:~:text=create) | - | | | [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch), [wrap_search_source_client.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/wrap_search_source_client.test.ts#:~:text=fetch) | - | | | [api.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts#:~:text=options) | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 20 more | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 5 more | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_sub_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 20 more | - | +| | [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [index.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=title), [use_rule_from_timeline.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.tsx#:~:text=title), [get_es_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/containers/detection_engine/exceptions/get_es_query_filter.ts#:~:text=title), [alerts_sub_grouping.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx#:~:text=title), [utils.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts#:~:text=title), [middleware.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=title), [get_query_filter.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/get_query_filter.ts#:~:text=title), [index_pattern.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts#:~:text=title), [utils.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/flyout_components/alerts_actions/utils.test.ts#:~:text=title)+ 5 more | - | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | | [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [policy_config.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/common/license/policy_config.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [fleet_integration.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [create_default_policy.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode), [license_watch.test.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts#:~:text=mode)+ 7 more | 8.8.0 | | | [query.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/query.ts#:~:text=license%24) | 8.8.0 | diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index 1a29e749e4dc7..a7dfcd567c0eb 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -7,7 +7,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team description: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 73de9827ebc22..e84818ff57329 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github description: API docs for the devTools plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] --- import devToolsObj from './dev_tools.devdocs.json'; diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index cde28fb9b20e0..ae5fc044dbe15 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github description: API docs for the discover plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] --- import discoverObj from './discover.devdocs.json'; diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 79aea49ce0239..0348a2e433ad1 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the discoverEnhanced plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index ec0192478b19a..14d381530a39e 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/embeddable.devdocs.json b/api_docs/embeddable.devdocs.json index 59ef54b7c5b64..1d2998c46bd41 100644 --- a/api_docs/embeddable.devdocs.json +++ b/api_docs/embeddable.devdocs.json @@ -5933,7 +5933,7 @@ "section": "def-common.ThemeServiceStart", "text": "ThemeServiceStart" }, - "; }) => ", + "; onAddPanel?: ((id: string) => void) | undefined; }) => ", { "pluginId": "@kbn/core-mount-utils-browser", "scope": "common", @@ -6243,6 +6243,38 @@ "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "embeddable", + "id": "def-public.openAddPanelFlyout.$1.onAddPanel", + "type": "Function", + "tags": [], + "label": "onAddPanel", + "description": [], + "signature": [ + "((id: string) => void) | undefined" + ], + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "embeddable", + "id": "def-public.openAddPanelFlyout.$1.onAddPanel.$1", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] } ] } diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 1de1e6f809d6b..73a43cf574ccf 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddable plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] --- import embeddableObj from './embeddable.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kib | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 544 | 11 | 442 | 4 | +| 546 | 11 | 444 | 4 | ## Client diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 24c39169fe91b..251b605711d5b 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the embeddableEnhanced plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] --- import embeddableEnhancedObj from './embeddable_enhanced.devdocs.json'; diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 6004d2b66f179..a5a1568454305 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the encryptedSavedObjects plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] --- import encryptedSavedObjectsObj from './encrypted_saved_objects.devdocs.json'; diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 68f4430bbeaae..6e28518c2984d 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the enterpriseSearch plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] --- import enterpriseSearchObj from './enterprise_search.devdocs.json'; diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 9e32f4ae6e75b..3e5429e314622 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github description: API docs for the esUiShared plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] --- import esUiSharedObj from './es_ui_shared.devdocs.json'; diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index 77ff102f16bcb..33152035253ec 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github description: API docs for the eventAnnotation plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] --- import eventAnnotationObj from './event_annotation.devdocs.json'; diff --git a/api_docs/event_log.devdocs.json b/api_docs/event_log.devdocs.json index b3075cced41d8..6d0ddc64e7fd3 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -1514,7 +1514,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" + "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false, @@ -1534,7 +1534,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1549,7 +1549,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" + "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; duration?: string | number | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 5e2e0f6c10292..5419658ffe67e 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github description: API docs for the eventLog plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 7315c69e13377..f6a72d2496321 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 4f23479cfbfc6..2c3e1cf2cf74a 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionError plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] --- import expressionErrorObj from './expression_error.devdocs.json'; diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 3c92eace2b8e3..6247a438a185e 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionGauge plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] --- import expressionGaugeObj from './expression_gauge.devdocs.json'; diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 0a35f4158d179..fd86849aaaae6 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionHeatmap plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] --- import expressionHeatmapObj from './expression_heatmap.devdocs.json'; diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 92cf80d1ab61a..938c8ccfae501 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionImage plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] --- import expressionImageObj from './expression_image.devdocs.json'; diff --git a/api_docs/expression_legacy_metric_vis.mdx b/api_docs/expression_legacy_metric_vis.mdx index 62c32bae5f5f1..a2b8ec78d3043 100644 --- a/api_docs/expression_legacy_metric_vis.mdx +++ b/api_docs/expression_legacy_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionLegacyMetricVis title: "expressionLegacyMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionLegacyMetricVis plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionLegacyMetricVis'] --- import expressionLegacyMetricVisObj from './expression_legacy_metric_vis.devdocs.json'; diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 8ccde120fd543..b40ce4eb2d895 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetric plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] --- import expressionMetricObj from './expression_metric.devdocs.json'; diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 6432d4714f1e3..dad1a3bd2c521 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionMetricVis plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] --- import expressionMetricVisObj from './expression_metric_vis.devdocs.json'; diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index d8a88c5d95960..355111b7a08f9 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionPartitionVis plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] --- import expressionPartitionVisObj from './expression_partition_vis.devdocs.json'; diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index a238dd8036fd1..8dc18439775b0 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRepeatImage plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] --- import expressionRepeatImageObj from './expression_repeat_image.devdocs.json'; diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index 333d1ba0e6264..0679966fa33ac 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionRevealImage plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] --- import expressionRevealImageObj from './expression_reveal_image.devdocs.json'; diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 079a9d1bbced9..91a6a255d1056 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionShape plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] --- import expressionShapeObj from './expression_shape.devdocs.json'; diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index 8af091771bd3e..151a78bae5094 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionTagcloud plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] --- import expressionTagcloudObj from './expression_tagcloud.devdocs.json'; diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index 1f6d6a9ba1c68..fe5a5e96b411b 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github description: API docs for the expressionXY plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] --- import expressionXYObj from './expression_x_y.devdocs.json'; diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index 631a20f0bcdf6..b6c561254afa3 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github description: API docs for the expressions plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] --- import expressionsObj from './expressions.devdocs.json'; diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 6dedd249971e1..2da84edae66dc 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github description: API docs for the features plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] --- import featuresObj from './features.devdocs.json'; diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index fc537a030e52b..9fb8bb5129081 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github description: API docs for the fieldFormats plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] --- import fieldFormatsObj from './field_formats.devdocs.json'; diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 624bbf01a981d..8f9d667848990 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github description: API docs for the fileUpload plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] --- import fileUploadObj from './file_upload.devdocs.json'; diff --git a/api_docs/files.devdocs.json b/api_docs/files.devdocs.json index a5d56dd4feea1..5ae8b6fe06dc3 100644 --- a/api_docs/files.devdocs.json +++ b/api_docs/files.devdocs.json @@ -305,7 +305,7 @@ "section": "def-common.FilesMetrics", "text": "FilesMetrics" }, - ">; publicDownload: (arg: Omit<{ token: string; fileName?: string | undefined; }, \"kind\">) => any; find: (arg: Omit<{ kind?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", + ">; publicDownload: (arg: Omit<{ token: string; fileName?: string | undefined; }, \"kind\">) => any; find: (arg: Omit<{ kind?: string | string[] | undefined; kindToExclude?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", { "pluginId": "@kbn/shared-ux-file-types", "scope": "common", @@ -649,9 +649,31 @@ "label": "FilesStart", "description": [], "signature": [ - "{ filesClientFactory: ", - "FilesClientFactory", - "; }" + "Pick<", + { + "pluginId": "files", + "scope": "public", + "docId": "kibFilesPluginApi", + "section": "def-public.FilesSetup", + "text": "FilesSetup" + }, + ", \"filesClientFactory\"> & { getFileKindDefinition: (id: string) => ", + { + "pluginId": "@kbn/shared-ux-file-types", + "scope": "common", + "docId": "kibKbnSharedUxFileTypesPluginApi", + "section": "def-common.FileKindBrowser", + "text": "FileKindBrowser" + }, + "; getAllFindKindDefinitions: () => ", + { + "pluginId": "@kbn/shared-ux-file-types", + "scope": "common", + "docId": "kibKbnSharedUxFileTypesPluginApi", + "section": "def-common.FileKindBrowser", + "text": "FileKindBrowser" + }, + "[]; }" ], "path": "src/plugins/files/public/plugin.ts", "deprecated": false, @@ -4145,6 +4167,22 @@ "deprecated": false, "trackAdoption": false }, + { + "parentPluginId": "files", + "id": "def-server.FindFileArgs.kindToExclude", + "type": "Array", + "tags": [], + "label": "kindToExclude", + "description": [ + "\nFile kind(s) to exclude from search, see {@link FileKind}." + ], + "signature": [ + "string[] | undefined" + ], + "path": "src/plugins/files/server/file_service/file_action_types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "files", "id": "def-server.FindFileArgs.name", @@ -5513,6 +5551,22 @@ "path": "packages/shared-ux/file/types/index.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "files", + "id": "def-common.FileKindBrowser.managementUiActions", + "type": "Object", + "tags": [], + "label": "managementUiActions", + "description": [ + "\nAllowed actions that can be done in the File Management UI. If not provided, all actions are allowed\n" + ], + "signature": [ + "{ list?: { enabled: boolean; } | undefined; delete?: { enabled: boolean; reason?: string | undefined; } | undefined; } | undefined" + ], + "path": "packages/shared-ux/file/types/index.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/files.mdx b/api_docs/files.mdx index 2e7af1d73120f..a5b8ee2210ad0 100644 --- a/api_docs/files.mdx +++ b/api_docs/files.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/files title: "files" image: https://source.unsplash.com/400x175/?github description: API docs for the files plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 215 | 0 | 10 | 5 | +| 217 | 0 | 10 | 5 | ## Client diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index 49c9a40ff8946..c09c2b6c46ab0 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index fe95c91406e4c..5ce7f17fbba77 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github description: API docs for the fleet plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] --- import fleetObj from './fleet.devdocs.json'; diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 37e44d7a88a33..cadcb64046cb3 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the globalSearch plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index 475a8a2c05da3..6b231e46f696e 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; diff --git a/api_docs/home.mdx b/api_docs/home.mdx index f62e10db50461..9c3bd452e4b3e 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github description: API docs for the home plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index 3477ece3819e0..0c1734a4fc1bc 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 51b3707ff4cda..923ea0cf3aa3e 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexLifecycleManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] --- import indexLifecycleManagementObj from './index_lifecycle_management.devdocs.json'; diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 7e62154cabf29..a05fd39524e3f 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the indexManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] --- import indexManagementObj from './index_management.devdocs.json'; diff --git a/api_docs/infra.devdocs.json b/api_docs/infra.devdocs.json index c9e30b7cf0fed..9807a95bd3e38 100644 --- a/api_docs/infra.devdocs.json +++ b/api_docs/infra.devdocs.json @@ -658,6 +658,20 @@ "deprecated": false, "trackAdoption": false, "children": [ + { + "parentPluginId": "infra", + "id": "def-server.InfraPluginStart.inventoryViews", + "type": "Object", + "tags": [], + "label": "inventoryViews", + "description": [], + "signature": [ + "InventoryViewsServiceStart" + ], + "path": "x-pack/plugins/infra/server/types.ts", + "deprecated": false, + "trackAdoption": false + }, { "parentPluginId": "infra", "id": "def-server.InfraPluginStart.logViews", diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index f95130c7fda77..01d3e95ad371c 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github description: API docs for the infra plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] --- import infraObj from './infra.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/inf | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 44 | 0 | 41 | 9 | +| 45 | 0 | 42 | 10 | ## Client diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 627ca307b241f..7d973531defa7 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github description: API docs for the inspector plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] --- import inspectorObj from './inspector.devdocs.json'; diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index 5794f30eaf867..133c39b185221 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github description: API docs for the interactiveSetup plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] --- import interactiveSetupObj from './interactive_setup.devdocs.json'; diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index ec79dddfe1fdb..52ec43f9cd7a6 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ace plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] --- import kbnAceObj from './kbn_ace.devdocs.json'; diff --git a/api_docs/kbn_aiops_components.mdx b/api_docs/kbn_aiops_components.mdx index 027622873bb5d..922819bab9d75 100644 --- a/api_docs/kbn_aiops_components.mdx +++ b/api_docs/kbn_aiops_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-components title: "@kbn/aiops-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-components plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-components'] --- import kbnAiopsComponentsObj from './kbn_aiops_components.devdocs.json'; diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index 3ba110b4d641e..f54e0a324c770 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/aiops-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 9209559d4c511..1f9b0096c77da 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index fd625f04df24b..c0711e797420b 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index 3529d77abdd16..aa8bf3cf410c9 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index c39f9c81be753..4ebd0dabd5566 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index 90704b67958d8..b85a463e73ea7 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 5bdc58d85dc6f..8e1d4258bfb99 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-client plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] --- import kbnAnalyticsClientObj from './kbn_analytics_client.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index 98e6154aa8a60..e759f05ccf0a7 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] --- import kbnAnalyticsShippersElasticV3BrowserObj from './kbn_analytics_shippers_elastic_v3_browser.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index 5cedf9017c0c3..d5cdbeb3d218c 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] --- import kbnAnalyticsShippersElasticV3CommonObj from './kbn_analytics_shippers_elastic_v3_common.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index 1900aafa94eb7..33b5b599ff9e5 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] --- import kbnAnalyticsShippersElasticV3ServerObj from './kbn_analytics_shippers_elastic_v3_server.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 506c2ef17b8a8..a4322612120fe 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index 51e1daf9c0a4c..9c2065eac13c2 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 28663f15a680c..119d56dbb3003 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-config-loader plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] --- import kbnApmConfigLoaderObj from './kbn_apm_config_loader.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace.mdx b/api_docs/kbn_apm_synthtrace.mdx index 1b0583f4267b7..984ef45d23120 100644 --- a/api_docs/kbn_apm_synthtrace.mdx +++ b/api_docs/kbn_apm_synthtrace.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace title: "@kbn/apm-synthtrace" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index 080fc9faa8d69..d4f621b3f9380 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 9509d2d37a5f2..53c237da8263f 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] --- import kbnApmUtilsObj from './kbn_apm_utils.devdocs.json'; diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index c9bac589f98e1..32b32fc6447e6 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/axe-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 9609f525002a3..9b53e44e3c6a2 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index 936150c592f65..06a1af606f4dd 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index 55c5bc95f3389..11a7574a92b3f 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index 81991f4c3596a..c45f16e2e80f5 100644 --- a/api_docs/kbn_chart_icons.mdx +++ b/api_docs/kbn_chart_icons.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-icons title: "@kbn/chart-icons" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-icons plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-icons'] --- import kbnChartIconsObj from './kbn_chart_icons.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 3eeac77d30d96..60bd4e161066f 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-core plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] --- import kbnCiStatsCoreObj from './kbn_ci_stats_core.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_performance_metrics.mdx b/api_docs/kbn_ci_stats_performance_metrics.mdx index 4210e48cd996c..33fc563195c53 100644 --- a/api_docs/kbn_ci_stats_performance_metrics.mdx +++ b/api_docs/kbn_ci_stats_performance_metrics.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-performance-metrics title: "@kbn/ci-stats-performance-metrics" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-performance-metrics plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-performance-metrics'] --- import kbnCiStatsPerformanceMetricsObj from './kbn_ci_stats_performance_metrics.devdocs.json'; diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index dd1eeae4185e8..14c447938b011 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ci-stats-reporter plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] --- import kbnCiStatsReporterObj from './kbn_ci_stats_reporter.devdocs.json'; diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 674d7849ea278..6799c9eb75262 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cli-dev-mode plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 2352502cb6168..e8336376665f3 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx index 299b419bc62ee..b0313bea59b09 100644 --- a/api_docs/kbn_code_editor_mocks.mdx +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mocks title: "@kbn/code-editor-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] --- import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index d0b05022cb683..9dd7c36be8f17 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/coloring plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] --- import kbnColoringObj from './kbn_coloring.devdocs.json'; diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 25ba7ec1f2e1f..f410ac6a9047f 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] --- import kbnConfigObj from './kbn_config.devdocs.json'; diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index c9cb8f54bf0b4..783c496cbd07b 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] --- import kbnConfigMocksObj from './kbn_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 11c17046984bd..0b2e28d2fcc01 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/config-schema plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 54ab8505a8c85..f86f0d415b6d4 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.devdocs.json b/api_docs/kbn_content_management_table_list.devdocs.json index 8f1e0a68c22ae..cfd72c12a42e5 100644 --- a/api_docs/kbn_content_management_table_list.devdocs.json +++ b/api_docs/kbn_content_management_table_list.devdocs.json @@ -35,7 +35,7 @@ "section": "def-common.UserContentCommonSchema", "text": "UserContentCommonSchema" }, - ">({ tableListTitle, tableListDescription, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, id: listingId, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, }: ", + ">({ tableListTitle, tableListDescription, entityName, entityNamePlural, initialFilter: initialQuery, headingId, initialPageSize, listingLimit, urlStateEnabled, customTableColumn, emptyPrompt, rowItemActions, findItems, createItem, editItem, deleteItems, getDetailViewLink, onClickTitle, id: listingId, contentEditor, children, titleColumnName, additionalRightSideActions, withoutPageTemplateWrapper, }: ", "Props", ") => JSX.Element | null" ], @@ -416,7 +416,23 @@ } ], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "@kbn/content-management-table-list", + "id": "def-common.RowActions", + "type": "Type", + "tags": [], + "label": "RowActions", + "description": [], + "signature": [ + "{ delete?: { enabled: boolean; reason?: string | undefined; } | undefined; }" + ], + "path": "packages/content-management/table_list/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], "objects": [] } } \ No newline at end of file diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 4c8960148995d..73765de18a394 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 13 | 5 | +| 21 | 0 | 14 | 5 | ## Common @@ -31,3 +31,6 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh ### Interfaces +### Consts, variables and types + + diff --git a/api_docs/kbn_content_management_utils.devdocs.json b/api_docs/kbn_content_management_utils.devdocs.json new file mode 100644 index 0000000000000..9f1ddd3e9ad60 --- /dev/null +++ b/api_docs/kbn_content_management_utils.devdocs.json @@ -0,0 +1,1201 @@ +{ + "id": "@kbn/content-management-utils", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes", + "type": "Interface", + "tags": [ + "argument", + "argument" + ], + "label": "ContentManagementCrudTypes", + "description": [ + "\nTypes used by content management storage" + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.ContentManagementCrudTypes", + "text": "ContentManagementCrudTypes" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.Item", + "type": "Object", + "tags": [], + "label": "Item", + "description": [ + "\nComplete saved object" + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.PartialItem", + "type": "CompoundType", + "tags": [], + "label": "PartialItem", + "description": [ + "\nPartial saved object, used as output for update" + ], + "signature": [ + "Omit<", + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + ", \"references\" | \"attributes\"> & { attributes: Partial; references: ", + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.Reference", + "text": "Reference" + }, + "[] | undefined; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.CreateOptions", + "type": "Uncategorized", + "tags": [], + "label": "CreateOptions", + "description": [ + "\nCreate options" + ], + "signature": [ + "CreateOptions" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.UpdateOptions", + "type": "Uncategorized", + "tags": [], + "label": "UpdateOptions", + "description": [ + "\nUpdate options" + ], + "signature": [ + "UpdateOptions" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.SearchOptions", + "type": "Uncategorized", + "tags": [], + "label": "SearchOptions", + "description": [ + "\nSearch options" + ], + "signature": [ + "SearchOptions" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.GetIn", + "type": "Object", + "tags": [], + "label": "GetIn", + "description": [ + "\nGet item params" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.GetIn", + "text": "GetIn" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.GetOut", + "type": "Object", + "tags": [], + "label": "GetOut", + "description": [ + "\nGet item result" + ], + "signature": [ + "{ item: ", + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + "; meta: { outcome: \"conflict\" | \"exactMatch\" | \"aliasMatch\"; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; }; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.CreateIn", + "type": "Object", + "tags": [], + "label": "CreateIn", + "description": [ + "\nCreate item params" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.CreateIn", + "text": "CreateIn" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.CreateOut", + "type": "Object", + "tags": [], + "label": "CreateOut", + "description": [ + "\nCreate item result" + ], + "signature": [ + "{ item: ", + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + "; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.SearchIn", + "type": "Object", + "tags": [], + "label": "SearchIn", + "description": [ + "\nSearch item params" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.SearchIn", + "text": "SearchIn" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.SearchOut", + "type": "Object", + "tags": [], + "label": "SearchOut", + "description": [ + "\nSearch item result" + ], + "signature": [ + "{ hits: ", + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + "[]; pagination: { total: number; cursor?: string | undefined; }; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.UpdateIn", + "type": "Object", + "tags": [], + "label": "UpdateIn", + "description": [ + "\nUpdate item params" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.UpdateIn", + "text": "UpdateIn" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.UpdateOut", + "type": "Object", + "tags": [], + "label": "UpdateOut", + "description": [ + "\nUpdate item result" + ], + "signature": [ + "{ item: PartialItem; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.DeleteIn", + "type": "Object", + "tags": [], + "label": "DeleteIn", + "description": [ + "\nDelete item params" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.DeleteIn", + "text": "DeleteIn" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.ContentManagementCrudTypes.DeleteOut", + "type": "Object", + "tags": [], + "label": "DeleteOut", + "description": [ + "\nDelete item result" + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "common", + "docId": "kibContentManagementPluginApi", + "section": "def-common.DeleteResult", + "text": "DeleteResult" + } + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.Reference", + "type": "Interface", + "tags": [], + "label": "Reference", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.Reference.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.Reference.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.Reference.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions", + "type": "Interface", + "tags": [], + "label": "SavedObjectCreateOptions", + "description": [ + "Saved Object create options - Pick and Omit to customize" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.id", + "type": "string", + "tags": [], + "label": "id", + "description": [ + "(not recommended) Specify an id for the document" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.overwrite", + "type": "CompoundType", + "tags": [], + "label": "overwrite", + "description": [ + "Overwrite existing documents (defaults to false)" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.version", + "type": "string", + "tags": [], + "label": "version", + "description": [ + "\nAn opaque version number which changes on each successful write operation.\nCan be used in conjunction with `overwrite` for implementing optimistic concurrency control." + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.references", + "type": "Array", + "tags": [], + "label": "references", + "description": [ + "Array of referenced saved objects." + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.Reference", + "text": "Reference" + }, + "[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.refresh", + "type": "CompoundType", + "tags": [], + "label": "refresh", + "description": [ + "The Elasticsearch Refresh setting for this operation" + ], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectCreateOptions.initialNamespaces", + "type": "Array", + "tags": [], + "label": "initialNamespaces", + "description": [ + "\nOptional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in\n{@link SavedObjectsCreateOptions}.\n\n* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces,\n including the \"All spaces\" identifier (`'*'`).\n* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only\n be used to specify a single space, and the \"All spaces\" identifier (`'*'`) is not allowed.\n* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used." + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions", + "type": "Interface", + "tags": [], + "label": "SavedObjectSearchOptions", + "description": [ + "Saved Object search options - Pick and Omit to customize" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.page", + "type": "number", + "tags": [], + "label": "page", + "description": [ + "the page of results to return" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.perPage", + "type": "number", + "tags": [], + "label": "perPage", + "description": [ + "the number of objects per page" + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.sortField", + "type": "string", + "tags": [], + "label": "sortField", + "description": [ + "which field to sort by" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.sortOrder", + "type": "CompoundType", + "tags": [], + "label": "sortOrder", + "description": [ + "sort order, ascending or descending" + ], + "signature": [ + "SortOrder", + " | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.fields", + "type": "Array", + "tags": [], + "label": "fields", + "description": [ + "\nAn array of fields to include in the results" + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.search", + "type": "string", + "tags": [], + "label": "search", + "description": [ + "Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String `query` argument for more information" + ], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.searchFields", + "type": "Array", + "tags": [], + "label": "searchFields", + "description": [ + "The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information" + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.searchAfter", + "type": "Array", + "tags": [], + "label": "searchAfter", + "description": [ + "\nUse the sort values from the previous page to retrieve the next page of results." + ], + "signature": [ + "SortResults", + " | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.rootSearchFields", + "type": "Array", + "tags": [], + "label": "rootSearchFields", + "description": [ + "\nThe fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not\nbe modified. If used in conjunction with `searchFields`, both are concatenated together." + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.hasReference", + "type": "CompoundType", + "tags": [], + "label": "hasReference", + "description": [ + "\nSearch for documents having a reference to the specified objects.\nUse `hasReferenceOperator` to specify the operator to use when searching for multiple references." + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + " | ", + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + "[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.hasReferenceOperator", + "type": "CompoundType", + "tags": [], + "label": "hasReferenceOperator", + "description": [ + "\nThe operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`" + ], + "signature": [ + "\"AND\" | \"OR\" | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.hasNoReference", + "type": "CompoundType", + "tags": [], + "label": "hasNoReference", + "description": [ + "\nSearch for documents *not* having a reference to the specified objects.\nUse `hasNoReferenceOperator` to specify the operator to use when searching for multiple references." + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + " | ", + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + "[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.hasNoReferenceOperator", + "type": "CompoundType", + "tags": [], + "label": "hasNoReferenceOperator", + "description": [ + "\nThe operator to use when searching by multiple references using the `hasNoReference` option. Defaults to `OR`" + ], + "signature": [ + "\"AND\" | \"OR\" | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.defaultSearchOperator", + "type": "CompoundType", + "tags": [], + "label": "defaultSearchOperator", + "description": [ + "\nThe search operator to use with the provided filter. Defaults to `OR`" + ], + "signature": [ + "\"AND\" | \"OR\" | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.filter", + "type": "Any", + "tags": [], + "label": "filter", + "description": [ + "filter string for the search query" + ], + "signature": [ + "any" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.aggs", + "type": "Object", + "tags": [ + "alpha" + ], + "label": "aggs", + "description": [ + "\nA record of aggregations to perform.\nThe API currently only supports a limited set of metrics and bucket aggregation types.\nAdditional aggregation types can be contributed to Core.\n" + ], + "signature": [ + "Record | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.namespaces", + "type": "Array", + "tags": [], + "label": "namespaces", + "description": [ + "array of namespaces to search" + ], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectSearchOptions.pit", + "type": "Object", + "tags": [], + "label": "pit", + "description": [ + "\nSearch against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}." + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsPitParams", + "text": "SavedObjectsPitParams" + }, + " | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions", + "type": "Interface", + "tags": [], + "label": "SavedObjectUpdateOptions", + "description": [ + "Saved Object update options - Pick and Omit to customize" + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SavedObjectUpdateOptions", + "text": "SavedObjectUpdateOptions" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions.references", + "type": "Array", + "tags": [], + "label": "references", + "description": [ + "Array of referenced saved objects." + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.Reference", + "text": "Reference" + }, + "[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions.version", + "type": "string", + "tags": [], + "label": "version", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions.refresh", + "type": "CompoundType", + "tags": [], + "label": "refresh", + "description": [ + "The Elasticsearch Refresh setting for this operation" + ], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.MutatingOperationRefreshSetting", + "text": "MutatingOperationRefreshSetting" + }, + " | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions.upsert", + "type": "Uncategorized", + "tags": [], + "label": "upsert", + "description": [ + "If specified, will be used to perform an upsert if the object doesn't exist" + ], + "signature": [ + "Attributes | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SavedObjectUpdateOptions.retryOnConflict", + "type": "number", + "tags": [], + "label": "retryOnConflict", + "description": [ + "\nThe Elasticsearch `retry_on_conflict` setting for this operation.\nDefaults to `0` when `version` is provided, `3` otherwise." + ], + "signature": [ + "number | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata", + "type": "Interface", + "tags": [], + "label": "SOWithMetadata", + "description": [ + "\nSaved object with metadata" + ], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.SOWithMetadata", + "text": "SOWithMetadata" + }, + "" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.version", + "type": "string", + "tags": [], + "label": "version", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.createdAt", + "type": "string", + "tags": [], + "label": "createdAt", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.updatedAt", + "type": "string", + "tags": [], + "label": "updatedAt", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.error", + "type": "Object", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "{ error: string; message: string; statusCode: number; metadata?: Record | undefined; } | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.attributes", + "type": "Uncategorized", + "tags": [], + "label": "attributes", + "description": [], + "signature": [ + "Attributes" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.references", + "type": "Array", + "tags": [], + "label": "references", + "description": [], + "signature": [ + { + "pluginId": "@kbn/content-management-utils", + "scope": "common", + "docId": "kibKbnContentManagementUtilsPluginApi", + "section": "def-common.Reference", + "text": "Reference" + }, + "[]" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.namespaces", + "type": "Array", + "tags": [], + "label": "namespaces", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.SOWithMetadata.originId", + "type": "string", + "tags": [], + "label": "originId", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/content-management-utils", + "id": "def-common.GetResultSO", + "type": "Type", + "tags": [], + "label": "GetResultSO", + "description": [ + "Return value for Saved Object get, T is item returned" + ], + "signature": [ + "{ item: T; meta: { outcome: \"conflict\" | \"exactMatch\" | \"aliasMatch\"; aliasTargetId?: string | undefined; aliasPurpose?: \"savedObjectConversion\" | \"savedObjectImport\" | undefined; }; }" + ], + "path": "packages/kbn-content-management-utils/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_content_management_utils.mdx b/api_docs/kbn_content_management_utils.mdx new file mode 100644 index 0000000000000..311ffb835ca3d --- /dev/null +++ b/api_docs/kbn_content_management_utils.mdx @@ -0,0 +1,33 @@ +--- +#### +#### This document is auto-generated and is meant to be viewed inside our experimental, new docs system. +#### Reach out in #docs-engineering for more info. +#### +id: kibKbnContentManagementUtilsPluginApi +slug: /kibana-dev-docs/api/kbn-content-management-utils +title: "@kbn/content-management-utils" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/content-management-utils plugin +date: 2023-04-25 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-utils'] +--- +import kbnContentManagementUtilsObj from './kbn_content_management_utils.devdocs.json'; + + + +Contact [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 64 | 1 | 15 | 0 | + +## Common + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 61fd9c2863537..b64655db6f085 100644 --- a/api_docs/kbn_core_analytics_browser.mdx +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser title: "@kbn/core-analytics-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] --- import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx index 8a3895b535171..f269813712914 100644 --- a/api_docs/kbn_core_analytics_browser_internal.mdx +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal title: "@kbn/core-analytics-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] --- import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx index 685a28bea6242..99a930f9d224f 100644 --- a/api_docs/kbn_core_analytics_browser_mocks.mdx +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks title: "@kbn/core-analytics-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] --- import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server.mdx b/api_docs/kbn_core_analytics_server.mdx index 5db1b0e6e16a6..a1ed9cf575661 100644 --- a/api_docs/kbn_core_analytics_server.mdx +++ b/api_docs/kbn_core_analytics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server title: "@kbn/core-analytics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server'] --- import kbnCoreAnalyticsServerObj from './kbn_core_analytics_server.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_internal.mdx b/api_docs/kbn_core_analytics_server_internal.mdx index 4272953c4d763..bfa8aecc9291f 100644 --- a/api_docs/kbn_core_analytics_server_internal.mdx +++ b/api_docs/kbn_core_analytics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-internal title: "@kbn/core-analytics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-internal'] --- import kbnCoreAnalyticsServerInternalObj from './kbn_core_analytics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_server_mocks.mdx b/api_docs/kbn_core_analytics_server_mocks.mdx index e0082181ed949..e66607e2cde5a 100644 --- a/api_docs/kbn_core_analytics_server_mocks.mdx +++ b/api_docs/kbn_core_analytics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-analytics-server-mocks title: "@kbn/core-analytics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-analytics-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-server-mocks'] --- import kbnCoreAnalyticsServerMocksObj from './kbn_core_analytics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser.mdx b/api_docs/kbn_core_application_browser.mdx index d3f1f8cfa0925..d7b3c1d1d5d2f 100644 --- a/api_docs/kbn_core_application_browser.mdx +++ b/api_docs/kbn_core_application_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser title: "@kbn/core-application-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser'] --- import kbnCoreApplicationBrowserObj from './kbn_core_application_browser.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_internal.mdx b/api_docs/kbn_core_application_browser_internal.mdx index 6ff37b3089b42..a1e664d7b5389 100644 --- a/api_docs/kbn_core_application_browser_internal.mdx +++ b/api_docs/kbn_core_application_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-internal title: "@kbn/core-application-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-internal'] --- import kbnCoreApplicationBrowserInternalObj from './kbn_core_application_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_application_browser_mocks.mdx b/api_docs/kbn_core_application_browser_mocks.mdx index 02a3fd158ec19..a745b6da5f6a0 100644 --- a/api_docs/kbn_core_application_browser_mocks.mdx +++ b/api_docs/kbn_core_application_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-browser-mocks title: "@kbn/core-application-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-browser-mocks'] --- import kbnCoreApplicationBrowserMocksObj from './kbn_core_application_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_application_common.mdx b/api_docs/kbn_core_application_common.mdx index d6a70b2b0157f..518ab4af770c2 100644 --- a/api_docs/kbn_core_application_common.mdx +++ b/api_docs/kbn_core_application_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-application-common title: "@kbn/core-application-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-application-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index d7c038b7a711f..de4d727c14fab 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index eb6b4e1963417..b87d5048a6905 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 1fcfd9f49e0fe..a3c8d5603427e 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 7d8bb7252a866..4914748e901c1 100644 --- a/api_docs/kbn_core_base_browser_mocks.mdx +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks title: "@kbn/core-base-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] --- import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 69cc08dec9190..40d46a7687fdd 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index eef24003ab660..1463fb87cb8fb 100644 --- a/api_docs/kbn_core_base_server_internal.mdx +++ b/api_docs/kbn_core_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-internal title: "@kbn/core-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-internal'] --- import kbnCoreBaseServerInternalObj from './kbn_core_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 58aff21c3f633..54c64f8b8fda9 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-base-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] --- import kbnCoreBaseServerMocksObj from './kbn_core_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_browser_mocks.mdx b/api_docs/kbn_core_capabilities_browser_mocks.mdx index 14f3d8712b6f3..0965192dcfdbe 100644 --- a/api_docs/kbn_core_capabilities_browser_mocks.mdx +++ b/api_docs/kbn_core_capabilities_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-browser-mocks title: "@kbn/core-capabilities-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-browser-mocks'] --- import kbnCoreCapabilitiesBrowserMocksObj from './kbn_core_capabilities_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_common.mdx b/api_docs/kbn_core_capabilities_common.mdx index d0b5db8e3f5b1..ad84e496b2079 100644 --- a/api_docs/kbn_core_capabilities_common.mdx +++ b/api_docs/kbn_core_capabilities_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-common title: "@kbn/core-capabilities-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-common'] --- import kbnCoreCapabilitiesCommonObj from './kbn_core_capabilities_common.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server.mdx b/api_docs/kbn_core_capabilities_server.mdx index ced47f174e1bf..8f38c2b6bdad6 100644 --- a/api_docs/kbn_core_capabilities_server.mdx +++ b/api_docs/kbn_core_capabilities_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server title: "@kbn/core-capabilities-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server'] --- import kbnCoreCapabilitiesServerObj from './kbn_core_capabilities_server.devdocs.json'; diff --git a/api_docs/kbn_core_capabilities_server_mocks.mdx b/api_docs/kbn_core_capabilities_server_mocks.mdx index 236d416f8d3f5..11d9eb0463909 100644 --- a/api_docs/kbn_core_capabilities_server_mocks.mdx +++ b/api_docs/kbn_core_capabilities_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-capabilities-server-mocks title: "@kbn/core-capabilities-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-capabilities-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-capabilities-server-mocks'] --- import kbnCoreCapabilitiesServerMocksObj from './kbn_core_capabilities_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser.mdx b/api_docs/kbn_core_chrome_browser.mdx index c286a69723583..6b89a10f6ec77 100644 --- a/api_docs/kbn_core_chrome_browser.mdx +++ b/api_docs/kbn_core_chrome_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser title: "@kbn/core-chrome-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser'] --- import kbnCoreChromeBrowserObj from './kbn_core_chrome_browser.devdocs.json'; diff --git a/api_docs/kbn_core_chrome_browser_mocks.mdx b/api_docs/kbn_core_chrome_browser_mocks.mdx index c4201d23a52f5..22da384234ced 100644 --- a/api_docs/kbn_core_chrome_browser_mocks.mdx +++ b/api_docs/kbn_core_chrome_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-chrome-browser-mocks title: "@kbn/core-chrome-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-chrome-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-chrome-browser-mocks'] --- import kbnCoreChromeBrowserMocksObj from './kbn_core_chrome_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_config_server_internal.mdx b/api_docs/kbn_core_config_server_internal.mdx index 5524f5d6c7f3a..70fcaf9182e51 100644 --- a/api_docs/kbn_core_config_server_internal.mdx +++ b/api_docs/kbn_core_config_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-config-server-internal title: "@kbn/core-config-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-config-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-config-server-internal'] --- import kbnCoreConfigServerInternalObj from './kbn_core_config_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index c06544f094756..5031c4b861035 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index 47db10d316e4b..1de9b8ddba154 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 74eb25b3e0989..087844e3c88bf 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index 2852e74f1d272..80375fc935809 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index 6b39d83563325..8f06e9041f173 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index efcb0ef0d682e..b6d11a52ac6a1 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index 1b84c3249cedb..ccf31dd049ecc 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index d9183c76749eb..26a111f2dbf6b 100644 --- a/api_docs/kbn_core_deprecations_browser.mdx +++ b/api_docs/kbn_core_deprecations_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser title: "@kbn/core-deprecations-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser'] --- import kbnCoreDeprecationsBrowserObj from './kbn_core_deprecations_browser.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_internal.mdx b/api_docs/kbn_core_deprecations_browser_internal.mdx index 2dabde1158feb..d06dc3b228af6 100644 --- a/api_docs/kbn_core_deprecations_browser_internal.mdx +++ b/api_docs/kbn_core_deprecations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-internal title: "@kbn/core-deprecations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-internal'] --- import kbnCoreDeprecationsBrowserInternalObj from './kbn_core_deprecations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser_mocks.mdx b/api_docs/kbn_core_deprecations_browser_mocks.mdx index 1f150624000c0..a885d35bc4a9c 100644 --- a/api_docs/kbn_core_deprecations_browser_mocks.mdx +++ b/api_docs/kbn_core_deprecations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-browser-mocks title: "@kbn/core-deprecations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-browser-mocks'] --- import kbnCoreDeprecationsBrowserMocksObj from './kbn_core_deprecations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_common.mdx b/api_docs/kbn_core_deprecations_common.mdx index f6098418f65b8..e7d74aed91261 100644 --- a/api_docs/kbn_core_deprecations_common.mdx +++ b/api_docs/kbn_core_deprecations_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-common title: "@kbn/core-deprecations-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-common'] --- import kbnCoreDeprecationsCommonObj from './kbn_core_deprecations_common.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server.mdx b/api_docs/kbn_core_deprecations_server.mdx index 18f41498f2022..35fa1b024d886 100644 --- a/api_docs/kbn_core_deprecations_server.mdx +++ b/api_docs/kbn_core_deprecations_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server title: "@kbn/core-deprecations-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server'] --- import kbnCoreDeprecationsServerObj from './kbn_core_deprecations_server.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_internal.mdx b/api_docs/kbn_core_deprecations_server_internal.mdx index 4da7cae29afc1..0ba73bbd364fa 100644 --- a/api_docs/kbn_core_deprecations_server_internal.mdx +++ b/api_docs/kbn_core_deprecations_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-internal title: "@kbn/core-deprecations-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-internal'] --- import kbnCoreDeprecationsServerInternalObj from './kbn_core_deprecations_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_server_mocks.mdx b/api_docs/kbn_core_deprecations_server_mocks.mdx index 7efaa1d260ede..1c2e9f62733f0 100644 --- a/api_docs/kbn_core_deprecations_server_mocks.mdx +++ b/api_docs/kbn_core_deprecations_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-deprecations-server-mocks title: "@kbn/core-deprecations-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-deprecations-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-deprecations-server-mocks'] --- import kbnCoreDeprecationsServerMocksObj from './kbn_core_deprecations_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 3a8b4f7733b89..db91ea5304499 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] --- import kbnCoreDocLinksBrowserObj from './kbn_core_doc_links_browser.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index 6d30b7f184a66..cfe58fb7263b4 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] --- import kbnCoreDocLinksBrowserMocksObj from './kbn_core_doc_links_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 7e04c291cf2e5..7bdf93b209089 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] --- import kbnCoreDocLinksServerObj from './kbn_core_doc_links_server.devdocs.json'; diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index bd6d7e5360355..794d174977ede 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] --- import kbnCoreDocLinksServerMocksObj from './kbn_core_doc_links_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx index 108a226e77328..a85edbed68ebb 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-internal title: "@kbn/core-elasticsearch-client-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-internal'] --- import kbnCoreElasticsearchClientServerInternalObj from './kbn_core_elasticsearch_client_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx index 36da5cccdf92f..d3fd0ffaa55a9 100644 --- a/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_client_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-client-server-mocks title: "@kbn/core-elasticsearch-client-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-client-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-client-server-mocks'] --- import kbnCoreElasticsearchClientServerMocksObj from './kbn_core_elasticsearch_client_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server.mdx b/api_docs/kbn_core_elasticsearch_server.mdx index a54b680f5d90d..f07a80ead0b91 100644 --- a/api_docs/kbn_core_elasticsearch_server.mdx +++ b/api_docs/kbn_core_elasticsearch_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server title: "@kbn/core-elasticsearch-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server'] --- import kbnCoreElasticsearchServerObj from './kbn_core_elasticsearch_server.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_internal.mdx b/api_docs/kbn_core_elasticsearch_server_internal.mdx index 22c91cf01d375..bb44b77ef391a 100644 --- a/api_docs/kbn_core_elasticsearch_server_internal.mdx +++ b/api_docs/kbn_core_elasticsearch_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-internal title: "@kbn/core-elasticsearch-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-internal'] --- import kbnCoreElasticsearchServerInternalObj from './kbn_core_elasticsearch_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_elasticsearch_server_mocks.mdx b/api_docs/kbn_core_elasticsearch_server_mocks.mdx index 50e70544b210b..be34c24b4d520 100644 --- a/api_docs/kbn_core_elasticsearch_server_mocks.mdx +++ b/api_docs/kbn_core_elasticsearch_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-elasticsearch-server-mocks title: "@kbn/core-elasticsearch-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-elasticsearch-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-elasticsearch-server-mocks'] --- import kbnCoreElasticsearchServerMocksObj from './kbn_core_elasticsearch_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_internal.mdx b/api_docs/kbn_core_environment_server_internal.mdx index f538a5f8e0b4e..e31b3a81bdabc 100644 --- a/api_docs/kbn_core_environment_server_internal.mdx +++ b/api_docs/kbn_core_environment_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-internal title: "@kbn/core-environment-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-internal'] --- import kbnCoreEnvironmentServerInternalObj from './kbn_core_environment_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_environment_server_mocks.mdx b/api_docs/kbn_core_environment_server_mocks.mdx index 8f1461db2e66f..d2e61d23962c0 100644 --- a/api_docs/kbn_core_environment_server_mocks.mdx +++ b/api_docs/kbn_core_environment_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-environment-server-mocks title: "@kbn/core-environment-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-environment-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-environment-server-mocks'] --- import kbnCoreEnvironmentServerMocksObj from './kbn_core_environment_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser.mdx b/api_docs/kbn_core_execution_context_browser.mdx index 77a377a7db9c4..6df94b463ca90 100644 --- a/api_docs/kbn_core_execution_context_browser.mdx +++ b/api_docs/kbn_core_execution_context_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser title: "@kbn/core-execution-context-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser'] --- import kbnCoreExecutionContextBrowserObj from './kbn_core_execution_context_browser.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_internal.mdx b/api_docs/kbn_core_execution_context_browser_internal.mdx index d7dcc38efe471..497fddcf7e231 100644 --- a/api_docs/kbn_core_execution_context_browser_internal.mdx +++ b/api_docs/kbn_core_execution_context_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-internal title: "@kbn/core-execution-context-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-internal'] --- import kbnCoreExecutionContextBrowserInternalObj from './kbn_core_execution_context_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_browser_mocks.mdx b/api_docs/kbn_core_execution_context_browser_mocks.mdx index d1ac9f6597bd6..856e7f9d76fb4 100644 --- a/api_docs/kbn_core_execution_context_browser_mocks.mdx +++ b/api_docs/kbn_core_execution_context_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-browser-mocks title: "@kbn/core-execution-context-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-browser-mocks'] --- import kbnCoreExecutionContextBrowserMocksObj from './kbn_core_execution_context_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_common.mdx b/api_docs/kbn_core_execution_context_common.mdx index 4d4c0cdc35fd7..e06e18473ee93 100644 --- a/api_docs/kbn_core_execution_context_common.mdx +++ b/api_docs/kbn_core_execution_context_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-common title: "@kbn/core-execution-context-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-common'] --- import kbnCoreExecutionContextCommonObj from './kbn_core_execution_context_common.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server.mdx b/api_docs/kbn_core_execution_context_server.mdx index d81bd38aa1916..a7aaa90cb5c41 100644 --- a/api_docs/kbn_core_execution_context_server.mdx +++ b/api_docs/kbn_core_execution_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server title: "@kbn/core-execution-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server'] --- import kbnCoreExecutionContextServerObj from './kbn_core_execution_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_internal.mdx b/api_docs/kbn_core_execution_context_server_internal.mdx index 05d5f2ae35765..dbf268c236e98 100644 --- a/api_docs/kbn_core_execution_context_server_internal.mdx +++ b/api_docs/kbn_core_execution_context_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-internal title: "@kbn/core-execution-context-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-internal'] --- import kbnCoreExecutionContextServerInternalObj from './kbn_core_execution_context_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_execution_context_server_mocks.mdx b/api_docs/kbn_core_execution_context_server_mocks.mdx index 68eb012274a21..13fccc87faa20 100644 --- a/api_docs/kbn_core_execution_context_server_mocks.mdx +++ b/api_docs/kbn_core_execution_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-execution-context-server-mocks title: "@kbn/core-execution-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-execution-context-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-execution-context-server-mocks'] --- import kbnCoreExecutionContextServerMocksObj from './kbn_core_execution_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser.mdx b/api_docs/kbn_core_fatal_errors_browser.mdx index 4bdeef98bbfde..48cac2e369e1d 100644 --- a/api_docs/kbn_core_fatal_errors_browser.mdx +++ b/api_docs/kbn_core_fatal_errors_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser title: "@kbn/core-fatal-errors-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser'] --- import kbnCoreFatalErrorsBrowserObj from './kbn_core_fatal_errors_browser.devdocs.json'; diff --git a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx index d94568e3ed0e6..2b5d84f23701a 100644 --- a/api_docs/kbn_core_fatal_errors_browser_mocks.mdx +++ b/api_docs/kbn_core_fatal_errors_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-fatal-errors-browser-mocks title: "@kbn/core-fatal-errors-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-fatal-errors-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-fatal-errors-browser-mocks'] --- import kbnCoreFatalErrorsBrowserMocksObj from './kbn_core_fatal_errors_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser.mdx b/api_docs/kbn_core_http_browser.mdx index 306f52219527d..acf212d2c6846 100644 --- a/api_docs/kbn_core_http_browser.mdx +++ b/api_docs/kbn_core_http_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser title: "@kbn/core-http-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser'] --- import kbnCoreHttpBrowserObj from './kbn_core_http_browser.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_internal.mdx b/api_docs/kbn_core_http_browser_internal.mdx index ab7a2167ee8d3..8e3cf5be8daf6 100644 --- a/api_docs/kbn_core_http_browser_internal.mdx +++ b/api_docs/kbn_core_http_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-internal title: "@kbn/core-http-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-internal'] --- import kbnCoreHttpBrowserInternalObj from './kbn_core_http_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_browser_mocks.mdx b/api_docs/kbn_core_http_browser_mocks.mdx index 3a484f5c5bb94..ebb1175bcb40a 100644 --- a/api_docs/kbn_core_http_browser_mocks.mdx +++ b/api_docs/kbn_core_http_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-browser-mocks title: "@kbn/core-http-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-browser-mocks'] --- import kbnCoreHttpBrowserMocksObj from './kbn_core_http_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_common.mdx b/api_docs/kbn_core_http_common.mdx index 4c6f719e62bb6..4728e428fba30 100644 --- a/api_docs/kbn_core_http_common.mdx +++ b/api_docs/kbn_core_http_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-common title: "@kbn/core-http-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-common'] --- import kbnCoreHttpCommonObj from './kbn_core_http_common.devdocs.json'; diff --git a/api_docs/kbn_core_http_context_server_mocks.mdx b/api_docs/kbn_core_http_context_server_mocks.mdx index 867ab6116747a..6fd18f3be652a 100644 --- a/api_docs/kbn_core_http_context_server_mocks.mdx +++ b/api_docs/kbn_core_http_context_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-context-server-mocks title: "@kbn/core-http-context-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-context-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-context-server-mocks'] --- import kbnCoreHttpContextServerMocksObj from './kbn_core_http_context_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index ad72f4f7242bc..8bd37f962ce10 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 8bca969b22064..24d10c696553f 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 408b53256f7db..bed254fe88fcd 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index c87dfa9f9fccf..a23262b66cac9 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_internal.mdx b/api_docs/kbn_core_http_router_server_internal.mdx index f165267f56e33..a56c06d0404df 100644 --- a/api_docs/kbn_core_http_router_server_internal.mdx +++ b/api_docs/kbn_core_http_router_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-internal title: "@kbn/core-http-router-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-internal'] --- import kbnCoreHttpRouterServerInternalObj from './kbn_core_http_router_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_router_server_mocks.mdx b/api_docs/kbn_core_http_router_server_mocks.mdx index e9e7dc64f0942..79d55f9e4eeb8 100644 --- a/api_docs/kbn_core_http_router_server_mocks.mdx +++ b/api_docs/kbn_core_http_router_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-router-server-mocks title: "@kbn/core-http-router-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-router-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-router-server-mocks'] --- import kbnCoreHttpRouterServerMocksObj from './kbn_core_http_router_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_http_server.mdx b/api_docs/kbn_core_http_server.mdx index f0cdaa85851dc..067b9306999e9 100644 --- a/api_docs/kbn_core_http_server.mdx +++ b/api_docs/kbn_core_http_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server title: "@kbn/core-http-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server'] --- import kbnCoreHttpServerObj from './kbn_core_http_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_internal.mdx b/api_docs/kbn_core_http_server_internal.mdx index f38f74d167a98..3914ccd95f841 100644 --- a/api_docs/kbn_core_http_server_internal.mdx +++ b/api_docs/kbn_core_http_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-internal title: "@kbn/core-http-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-internal'] --- import kbnCoreHttpServerInternalObj from './kbn_core_http_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_server_mocks.mdx b/api_docs/kbn_core_http_server_mocks.mdx index c59347545a328..df11606798683 100644 --- a/api_docs/kbn_core_http_server_mocks.mdx +++ b/api_docs/kbn_core_http_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-server-mocks title: "@kbn/core-http-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-server-mocks'] --- import kbnCoreHttpServerMocksObj from './kbn_core_http_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser.mdx b/api_docs/kbn_core_i18n_browser.mdx index c38585619f6c0..a3ce906c0279d 100644 --- a/api_docs/kbn_core_i18n_browser.mdx +++ b/api_docs/kbn_core_i18n_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser title: "@kbn/core-i18n-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser'] --- import kbnCoreI18nBrowserObj from './kbn_core_i18n_browser.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_browser_mocks.mdx b/api_docs/kbn_core_i18n_browser_mocks.mdx index 0fe31a6d87fa1..45a7e9042ce1b 100644 --- a/api_docs/kbn_core_i18n_browser_mocks.mdx +++ b/api_docs/kbn_core_i18n_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-browser-mocks title: "@kbn/core-i18n-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-browser-mocks'] --- import kbnCoreI18nBrowserMocksObj from './kbn_core_i18n_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server.mdx b/api_docs/kbn_core_i18n_server.mdx index a9a56e81b982d..609a0a4818ab3 100644 --- a/api_docs/kbn_core_i18n_server.mdx +++ b/api_docs/kbn_core_i18n_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server title: "@kbn/core-i18n-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server'] --- import kbnCoreI18nServerObj from './kbn_core_i18n_server.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_internal.mdx b/api_docs/kbn_core_i18n_server_internal.mdx index ec02bcea6b7f7..37c4c3ee83c08 100644 --- a/api_docs/kbn_core_i18n_server_internal.mdx +++ b/api_docs/kbn_core_i18n_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-internal title: "@kbn/core-i18n-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-internal'] --- import kbnCoreI18nServerInternalObj from './kbn_core_i18n_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_i18n_server_mocks.mdx b/api_docs/kbn_core_i18n_server_mocks.mdx index 1a2c897736245..d84bd90a40383 100644 --- a/api_docs/kbn_core_i18n_server_mocks.mdx +++ b/api_docs/kbn_core_i18n_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-i18n-server-mocks title: "@kbn/core-i18n-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-i18n-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-i18n-server-mocks'] --- import kbnCoreI18nServerMocksObj from './kbn_core_i18n_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 3c162c9189752..530e498c01a25 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] --- import kbnCoreInjectedMetadataBrowserMocksObj from './kbn_core_injected_metadata_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_internal.mdx b/api_docs/kbn_core_integrations_browser_internal.mdx index 494208c4c5914..591163fb8790d 100644 --- a/api_docs/kbn_core_integrations_browser_internal.mdx +++ b/api_docs/kbn_core_integrations_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-internal title: "@kbn/core-integrations-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-internal'] --- import kbnCoreIntegrationsBrowserInternalObj from './kbn_core_integrations_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_integrations_browser_mocks.mdx b/api_docs/kbn_core_integrations_browser_mocks.mdx index a037d65732f72..d4657d880f3f5 100644 --- a/api_docs/kbn_core_integrations_browser_mocks.mdx +++ b/api_docs/kbn_core_integrations_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-integrations-browser-mocks title: "@kbn/core-integrations-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-integrations-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-integrations-browser-mocks'] --- import kbnCoreIntegrationsBrowserMocksObj from './kbn_core_integrations_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index 99067d79f0a9f..171b4c8439822 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index 534bb78d4eaba..aa224ae9c82c7 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index 81ac149482e3c..5f88d588307f3 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index 26ba420c08d5c..b5fb67855d772 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 9ba0f825772fa..42cd892938479 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index a960a65461e1d..5cbeb9a6b2d77 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 3fd09f6263c27..8f2e8da57a9d0 100644 --- a/api_docs/kbn_core_logging_server.mdx +++ b/api_docs/kbn_core_logging_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server title: "@kbn/core-logging-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server'] --- import kbnCoreLoggingServerObj from './kbn_core_logging_server.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_internal.mdx b/api_docs/kbn_core_logging_server_internal.mdx index f3acbc7fc8045..203f90c1d41ee 100644 --- a/api_docs/kbn_core_logging_server_internal.mdx +++ b/api_docs/kbn_core_logging_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-internal title: "@kbn/core-logging-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-internal'] --- import kbnCoreLoggingServerInternalObj from './kbn_core_logging_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server_mocks.mdx b/api_docs/kbn_core_logging_server_mocks.mdx index c3ea743d26d84..b0fcac62cba04 100644 --- a/api_docs/kbn_core_logging_server_mocks.mdx +++ b/api_docs/kbn_core_logging_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-server-mocks title: "@kbn/core-logging-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-server-mocks'] --- import kbnCoreLoggingServerMocksObj from './kbn_core_logging_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_internal.mdx b/api_docs/kbn_core_metrics_collectors_server_internal.mdx index 71e8faae9eed4..95725dc4f0853 100644 --- a/api_docs/kbn_core_metrics_collectors_server_internal.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-internal title: "@kbn/core-metrics-collectors-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-internal'] --- import kbnCoreMetricsCollectorsServerInternalObj from './kbn_core_metrics_collectors_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx index 2eabbc2bacc62..8601cebc03b08 100644 --- a/api_docs/kbn_core_metrics_collectors_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_collectors_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-collectors-server-mocks title: "@kbn/core-metrics-collectors-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-collectors-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-collectors-server-mocks'] --- import kbnCoreMetricsCollectorsServerMocksObj from './kbn_core_metrics_collectors_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server.mdx b/api_docs/kbn_core_metrics_server.mdx index f37d52033dd7a..2b6f90835423c 100644 --- a/api_docs/kbn_core_metrics_server.mdx +++ b/api_docs/kbn_core_metrics_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server title: "@kbn/core-metrics-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server'] --- import kbnCoreMetricsServerObj from './kbn_core_metrics_server.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_internal.mdx b/api_docs/kbn_core_metrics_server_internal.mdx index a7217eaf16db7..fd0f9f47b6c60 100644 --- a/api_docs/kbn_core_metrics_server_internal.mdx +++ b/api_docs/kbn_core_metrics_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-internal title: "@kbn/core-metrics-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-internal'] --- import kbnCoreMetricsServerInternalObj from './kbn_core_metrics_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_metrics_server_mocks.mdx b/api_docs/kbn_core_metrics_server_mocks.mdx index 59ea42f319e9d..38722e9154916 100644 --- a/api_docs/kbn_core_metrics_server_mocks.mdx +++ b/api_docs/kbn_core_metrics_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-metrics-server-mocks title: "@kbn/core-metrics-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-metrics-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-metrics-server-mocks'] --- import kbnCoreMetricsServerMocksObj from './kbn_core_metrics_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_mount_utils_browser.mdx b/api_docs/kbn_core_mount_utils_browser.mdx index 466745e2ce499..7e520e3b42b45 100644 --- a/api_docs/kbn_core_mount_utils_browser.mdx +++ b/api_docs/kbn_core_mount_utils_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-mount-utils-browser title: "@kbn/core-mount-utils-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-mount-utils-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-mount-utils-browser'] --- import kbnCoreMountUtilsBrowserObj from './kbn_core_mount_utils_browser.devdocs.json'; diff --git a/api_docs/kbn_core_node_server.mdx b/api_docs/kbn_core_node_server.mdx index 98986e42b5bb0..b6faa54b4347e 100644 --- a/api_docs/kbn_core_node_server.mdx +++ b/api_docs/kbn_core_node_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server title: "@kbn/core-node-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server'] --- import kbnCoreNodeServerObj from './kbn_core_node_server.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_internal.mdx b/api_docs/kbn_core_node_server_internal.mdx index d8b7087087930..42104c5c02645 100644 --- a/api_docs/kbn_core_node_server_internal.mdx +++ b/api_docs/kbn_core_node_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-internal title: "@kbn/core-node-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-internal'] --- import kbnCoreNodeServerInternalObj from './kbn_core_node_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_node_server_mocks.mdx b/api_docs/kbn_core_node_server_mocks.mdx index b45a40d461b52..f2069ec00fad8 100644 --- a/api_docs/kbn_core_node_server_mocks.mdx +++ b/api_docs/kbn_core_node_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-node-server-mocks title: "@kbn/core-node-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-node-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-node-server-mocks'] --- import kbnCoreNodeServerMocksObj from './kbn_core_node_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser.mdx b/api_docs/kbn_core_notifications_browser.mdx index c8536a941c46f..afa264cdbe4b2 100644 --- a/api_docs/kbn_core_notifications_browser.mdx +++ b/api_docs/kbn_core_notifications_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser title: "@kbn/core-notifications-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser'] --- import kbnCoreNotificationsBrowserObj from './kbn_core_notifications_browser.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_internal.mdx b/api_docs/kbn_core_notifications_browser_internal.mdx index 7146b20a88240..847bf67c4aa8d 100644 --- a/api_docs/kbn_core_notifications_browser_internal.mdx +++ b/api_docs/kbn_core_notifications_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-internal title: "@kbn/core-notifications-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-internal'] --- import kbnCoreNotificationsBrowserInternalObj from './kbn_core_notifications_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_notifications_browser_mocks.mdx b/api_docs/kbn_core_notifications_browser_mocks.mdx index 2518700bd2a9d..0d34a5cd902a3 100644 --- a/api_docs/kbn_core_notifications_browser_mocks.mdx +++ b/api_docs/kbn_core_notifications_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-notifications-browser-mocks title: "@kbn/core-notifications-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-notifications-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-notifications-browser-mocks'] --- import kbnCoreNotificationsBrowserMocksObj from './kbn_core_notifications_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser.mdx b/api_docs/kbn_core_overlays_browser.mdx index 38f32ad1bd2c1..3fd6130cdb8d8 100644 --- a/api_docs/kbn_core_overlays_browser.mdx +++ b/api_docs/kbn_core_overlays_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser title: "@kbn/core-overlays-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser'] --- import kbnCoreOverlaysBrowserObj from './kbn_core_overlays_browser.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_internal.mdx b/api_docs/kbn_core_overlays_browser_internal.mdx index d158e2d4ff953..8ecd79bb3a3e8 100644 --- a/api_docs/kbn_core_overlays_browser_internal.mdx +++ b/api_docs/kbn_core_overlays_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-internal title: "@kbn/core-overlays-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-internal'] --- import kbnCoreOverlaysBrowserInternalObj from './kbn_core_overlays_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_overlays_browser_mocks.mdx b/api_docs/kbn_core_overlays_browser_mocks.mdx index e2c79208c0674..2fc8b367d6fdd 100644 --- a/api_docs/kbn_core_overlays_browser_mocks.mdx +++ b/api_docs/kbn_core_overlays_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-overlays-browser-mocks title: "@kbn/core-overlays-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-overlays-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-overlays-browser-mocks'] --- import kbnCoreOverlaysBrowserMocksObj from './kbn_core_overlays_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 8d655eb267f5a..6fb4b8ec4c90e 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 77ea3d979a1fa..4705b9ed25bf7 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 44b22b20623c4..8b10022d6b74e 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index c032049f2dd4f..3404d9772c1ad 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 08eca2b502206..787d6e83c77ae 100644 --- a/api_docs/kbn_core_preboot_server.mdx +++ b/api_docs/kbn_core_preboot_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server title: "@kbn/core-preboot-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server'] --- import kbnCorePrebootServerObj from './kbn_core_preboot_server.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server_mocks.mdx b/api_docs/kbn_core_preboot_server_mocks.mdx index 971dc60fbd402..f5261db81d189 100644 --- a/api_docs/kbn_core_preboot_server_mocks.mdx +++ b/api_docs/kbn_core_preboot_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-preboot-server-mocks title: "@kbn/core-preboot-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-preboot-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-preboot-server-mocks'] --- import kbnCorePrebootServerMocksObj from './kbn_core_preboot_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_browser_mocks.mdx b/api_docs/kbn_core_rendering_browser_mocks.mdx index f7cb4f618bbf9..af9157cecdeaf 100644 --- a/api_docs/kbn_core_rendering_browser_mocks.mdx +++ b/api_docs/kbn_core_rendering_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-browser-mocks title: "@kbn/core-rendering-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-browser-mocks'] --- import kbnCoreRenderingBrowserMocksObj from './kbn_core_rendering_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 91d2f9cd5b3a7..0dd9d376a4e67 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index e114fbfb6c870..142f2fa27e62a 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 12234cdb3c57c..eee680ca1b5e1 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_browser.mdx b/api_docs/kbn_core_saved_objects_api_browser.mdx index 2ae4ebd0c693b..dfaeecf2543c4 100644 --- a/api_docs/kbn_core_saved_objects_api_browser.mdx +++ b/api_docs/kbn_core_saved_objects_api_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-browser title: "@kbn/core-saved-objects-api-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-browser'] --- import kbnCoreSavedObjectsApiBrowserObj from './kbn_core_saved_objects_api_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server.mdx b/api_docs/kbn_core_saved_objects_api_server.mdx index 55646f1f382b6..d04aab2eb21b9 100644 --- a/api_docs/kbn_core_saved_objects_api_server.mdx +++ b/api_docs/kbn_core_saved_objects_api_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server title: "@kbn/core-saved-objects-api-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server'] --- import kbnCoreSavedObjectsApiServerObj from './kbn_core_saved_objects_api_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_internal.mdx b/api_docs/kbn_core_saved_objects_api_server_internal.mdx index 165d452af821e..7cbd01cb6659d 100644 --- a/api_docs/kbn_core_saved_objects_api_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-internal title: "@kbn/core-saved-objects-api-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-internal'] --- import kbnCoreSavedObjectsApiServerInternalObj from './kbn_core_saved_objects_api_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx index 99c35f7983158..ccbf3695bf3d2 100644 --- a/api_docs/kbn_core_saved_objects_api_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_api_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-api-server-mocks title: "@kbn/core-saved-objects-api-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-api-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-api-server-mocks'] --- import kbnCoreSavedObjectsApiServerMocksObj from './kbn_core_saved_objects_api_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_internal.mdx b/api_docs/kbn_core_saved_objects_base_server_internal.mdx index 1978b36015f7e..128e9e835c029 100644 --- a/api_docs/kbn_core_saved_objects_base_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-internal title: "@kbn/core-saved-objects-base-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-internal'] --- import kbnCoreSavedObjectsBaseServerInternalObj from './kbn_core_saved_objects_base_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx index 79c73b9220d0c..d0775b712d605 100644 --- a/api_docs/kbn_core_saved_objects_base_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_base_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-base-server-mocks title: "@kbn/core-saved-objects-base-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-base-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-base-server-mocks'] --- import kbnCoreSavedObjectsBaseServerMocksObj from './kbn_core_saved_objects_base_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser.mdx b/api_docs/kbn_core_saved_objects_browser.mdx index 10eb5868ecf47..6f1f46aa38e73 100644 --- a/api_docs/kbn_core_saved_objects_browser.mdx +++ b/api_docs/kbn_core_saved_objects_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser title: "@kbn/core-saved-objects-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser'] --- import kbnCoreSavedObjectsBrowserObj from './kbn_core_saved_objects_browser.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_internal.mdx b/api_docs/kbn_core_saved_objects_browser_internal.mdx index 4615be9c11ce1..86d42b6b8c608 100644 --- a/api_docs/kbn_core_saved_objects_browser_internal.mdx +++ b/api_docs/kbn_core_saved_objects_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-internal title: "@kbn/core-saved-objects-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-internal'] --- import kbnCoreSavedObjectsBrowserInternalObj from './kbn_core_saved_objects_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_browser_mocks.mdx b/api_docs/kbn_core_saved_objects_browser_mocks.mdx index a37c978cfb343..d65aee63687a2 100644 --- a/api_docs/kbn_core_saved_objects_browser_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-browser-mocks title: "@kbn/core-saved-objects-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-browser-mocks'] --- import kbnCoreSavedObjectsBrowserMocksObj from './kbn_core_saved_objects_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_common.mdx b/api_docs/kbn_core_saved_objects_common.mdx index 706710e2cb824..f676d4fca4630 100644 --- a/api_docs/kbn_core_saved_objects_common.mdx +++ b/api_docs/kbn_core_saved_objects_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-common title: "@kbn/core-saved-objects-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-common'] --- import kbnCoreSavedObjectsCommonObj from './kbn_core_saved_objects_common.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx index e0122dffcacb0..98be6ceff5983 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-internal title: "@kbn/core-saved-objects-import-export-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-internal'] --- import kbnCoreSavedObjectsImportExportServerInternalObj from './kbn_core_saved_objects_import_export_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx index 08cc43ae6a54d..4460fd0b02f89 100644 --- a/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_import_export_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-import-export-server-mocks title: "@kbn/core-saved-objects-import-export-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-import-export-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-import-export-server-mocks'] --- import kbnCoreSavedObjectsImportExportServerMocksObj from './kbn_core_saved_objects_import_export_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx index 49dcee745ce38..38281e01c8bbe 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-internal title: "@kbn/core-saved-objects-migration-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-internal'] --- import kbnCoreSavedObjectsMigrationServerInternalObj from './kbn_core_saved_objects_migration_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx index 612f215e8d922..47e427a65c326 100644 --- a/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_migration_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-migration-server-mocks title: "@kbn/core-saved-objects-migration-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-migration-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-migration-server-mocks'] --- import kbnCoreSavedObjectsMigrationServerMocksObj from './kbn_core_saved_objects_migration_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server.mdx b/api_docs/kbn_core_saved_objects_server.mdx index 391665fd14f8c..60e7cc8bfb088 100644 --- a/api_docs/kbn_core_saved_objects_server.mdx +++ b/api_docs/kbn_core_saved_objects_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server title: "@kbn/core-saved-objects-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server'] --- import kbnCoreSavedObjectsServerObj from './kbn_core_saved_objects_server.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_internal.mdx b/api_docs/kbn_core_saved_objects_server_internal.mdx index 8f7af4ecd077d..63c8b4908cd45 100644 --- a/api_docs/kbn_core_saved_objects_server_internal.mdx +++ b/api_docs/kbn_core_saved_objects_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-internal title: "@kbn/core-saved-objects-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-internal'] --- import kbnCoreSavedObjectsServerInternalObj from './kbn_core_saved_objects_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_server_mocks.mdx b/api_docs/kbn_core_saved_objects_server_mocks.mdx index 0a1e5d1292b0a..3fef61e10359b 100644 --- a/api_docs/kbn_core_saved_objects_server_mocks.mdx +++ b/api_docs/kbn_core_saved_objects_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-server-mocks title: "@kbn/core-saved-objects-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-server-mocks'] --- import kbnCoreSavedObjectsServerMocksObj from './kbn_core_saved_objects_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_saved_objects_utils_server.mdx b/api_docs/kbn_core_saved_objects_utils_server.mdx index b99ce47f08e8a..9fa7764ac307f 100644 --- a/api_docs/kbn_core_saved_objects_utils_server.mdx +++ b/api_docs/kbn_core_saved_objects_utils_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-saved-objects-utils-server title: "@kbn/core-saved-objects-utils-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-saved-objects-utils-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-saved-objects-utils-server'] --- import kbnCoreSavedObjectsUtilsServerObj from './kbn_core_saved_objects_utils_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_common.mdx b/api_docs/kbn_core_status_common.mdx index de9b1e18cae5d..9bbd560f514cd 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common title: "@kbn/core-status-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 301494e7b2388..1fcb770a7f836 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-common-internal title: "@kbn/core-status-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-common-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index df15953abc315..ac91c3bbbe491 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server title: "@kbn/core-status-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index 075a12e1c6f24..3b84bab2c12fa 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-internal title: "@kbn/core-status-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index d47e6f46490f1..545b8c3c51a64 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-status-server-mocks title: "@kbn/core-status-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-status-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index a38afc5d4cc28..f3fc43285ef75 100644 --- a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx +++ b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-deprecations-getters title: "@kbn/core-test-helpers-deprecations-getters" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-deprecations-getters plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-deprecations-getters'] --- import kbnCoreTestHelpersDeprecationsGettersObj from './kbn_core_test_helpers_deprecations_getters.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx index ef13c55e07c61..fb6c5ca01698c 100644 --- a/api_docs/kbn_core_test_helpers_http_setup_browser.mdx +++ b/api_docs/kbn_core_test_helpers_http_setup_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-http-setup-browser title: "@kbn/core-test-helpers-http-setup-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-http-setup-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-http-setup-browser'] --- import kbnCoreTestHelpersHttpSetupBrowserObj from './kbn_core_test_helpers_http_setup_browser.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index cf75dcf5478b4..aef8e4d42c71c 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index f11a869c92ee1..4374ec559a5d6 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 484dd22728fbc..e6b16af9bcb5d 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 63cd2de86d6e4..7383ce4111dae 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] --- import kbnCoreThemeBrowserObj from './kbn_core_theme_browser.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_internal.mdx b/api_docs/kbn_core_theme_browser_internal.mdx index 54a121b8df78a..1235740a86f62 100644 --- a/api_docs/kbn_core_theme_browser_internal.mdx +++ b/api_docs/kbn_core_theme_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-internal title: "@kbn/core-theme-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-internal'] --- import kbnCoreThemeBrowserInternalObj from './kbn_core_theme_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index a21b5fa4ab454..23b5c986ca522 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] --- import kbnCoreThemeBrowserMocksObj from './kbn_core_theme_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser.mdx b/api_docs/kbn_core_ui_settings_browser.mdx index e741c75cc66b0..2931039207d99 100644 --- a/api_docs/kbn_core_ui_settings_browser.mdx +++ b/api_docs/kbn_core_ui_settings_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser title: "@kbn/core-ui-settings-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser'] --- import kbnCoreUiSettingsBrowserObj from './kbn_core_ui_settings_browser.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_internal.mdx b/api_docs/kbn_core_ui_settings_browser_internal.mdx index 92c21046ca2bb..1889f6b78437a 100644 --- a/api_docs/kbn_core_ui_settings_browser_internal.mdx +++ b/api_docs/kbn_core_ui_settings_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-internal title: "@kbn/core-ui-settings-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-internal'] --- import kbnCoreUiSettingsBrowserInternalObj from './kbn_core_ui_settings_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_browser_mocks.mdx b/api_docs/kbn_core_ui_settings_browser_mocks.mdx index 9b8babfa1fcf3..7eb24fbc7fb7b 100644 --- a/api_docs/kbn_core_ui_settings_browser_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-browser-mocks title: "@kbn/core-ui-settings-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-browser-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-browser-mocks'] --- import kbnCoreUiSettingsBrowserMocksObj from './kbn_core_ui_settings_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_common.mdx b/api_docs/kbn_core_ui_settings_common.mdx index b95f699d6d050..2450d22c66fb1 100644 --- a/api_docs/kbn_core_ui_settings_common.mdx +++ b/api_docs/kbn_core_ui_settings_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-common title: "@kbn/core-ui-settings-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-common'] --- import kbnCoreUiSettingsCommonObj from './kbn_core_ui_settings_common.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index a88c23c489891..eaeb3e4a7e921 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 06432816c1788..631a6b4af48d1 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index e1477ab3eeb87..9be618bb23931 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index 914a5468667b5..336935840823f 100644 --- a/api_docs/kbn_core_usage_data_server.mdx +++ b/api_docs/kbn_core_usage_data_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server title: "@kbn/core-usage-data-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server'] --- import kbnCoreUsageDataServerObj from './kbn_core_usage_data_server.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_internal.mdx b/api_docs/kbn_core_usage_data_server_internal.mdx index b5596d8ce5bc7..5bff292e0ba66 100644 --- a/api_docs/kbn_core_usage_data_server_internal.mdx +++ b/api_docs/kbn_core_usage_data_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-internal title: "@kbn/core-usage-data-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-internal plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-internal'] --- import kbnCoreUsageDataServerInternalObj from './kbn_core_usage_data_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server_mocks.mdx b/api_docs/kbn_core_usage_data_server_mocks.mdx index c5fc08f39d31e..59d9348dc7110 100644 --- a/api_docs/kbn_core_usage_data_server_mocks.mdx +++ b/api_docs/kbn_core_usage_data_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-usage-data-server-mocks title: "@kbn/core-usage-data-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-usage-data-server-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-usage-data-server-mocks'] --- import kbnCoreUsageDataServerMocksObj from './kbn_core_usage_data_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index ff42ee5669ec3..382f353135287 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] --- import kbnCryptoObj from './kbn_crypto.devdocs.json'; diff --git a/api_docs/kbn_crypto_browser.mdx b/api_docs/kbn_crypto_browser.mdx index 6af342c88de2c..d0bd92809e486 100644 --- a/api_docs/kbn_crypto_browser.mdx +++ b/api_docs/kbn_crypto_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-crypto-browser title: "@kbn/crypto-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/crypto-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index 845a8d13f6268..ba85e5db86b15 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 3b0b33c77b227..d846010affe30 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/datemath plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] --- import kbnDatemathObj from './kbn_datemath.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index afafe20cb2e1e..dde7c69400f0c 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-errors plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] --- import kbnDevCliErrorsObj from './kbn_dev_cli_errors.devdocs.json'; diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index 4de5774e12693..266c8b2554645 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-cli-runner plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] --- import kbnDevCliRunnerObj from './kbn_dev_cli_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 683f4f5f7821b..e4d487eed68f8 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-proc-runner plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] --- import kbnDevProcRunnerObj from './kbn_dev_proc_runner.devdocs.json'; diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 36602e1f31d55..9e553a8d2b1f2 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dev-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] --- import kbnDevUtilsObj from './kbn_dev_utils.devdocs.json'; diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index b01beac37dc53..496205404ca2d 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -300,7 +300,7 @@ "label": "enterpriseSearch", "description": [], "signature": [ - "{ readonly apiKeys: string; readonly behavioralAnalytics: string; readonly behavioralAnalyticsEvents: string; readonly bulkApi: string; readonly configuration: string; readonly connectors: string; readonly connectorsAzureBlobStorage: string; readonly connectorsGoogleCloudStorage: string; readonly connectorsMicrosoftSQL: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsNetworkDrive: string; readonly connectorsOracle: string; readonly connectorsPostgreSQL: string; readonly connectorsS3: string; readonly connectorsWorkplaceSearch: string; readonly crawlerExtractionRules: string; readonly crawlerManaging: string; readonly crawlerOverview: string; readonly deployTrainedModels: string; readonly documentLevelSecurity: string; readonly elser: string; readonly engines: string; readonly ingestionApis: string; readonly ingestPipelines: string; readonly languageAnalyzers: string; readonly languageClients: string; readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; readonly usersAccess: string; }" + "{ readonly apiKeys: string; readonly behavioralAnalytics: string; readonly behavioralAnalyticsEvents: string; readonly bulkApi: string; readonly configuration: string; readonly connectors: string; readonly connectorsAzureBlobStorage: string; readonly connectorsGoogleCloudStorage: string; readonly connectorsMicrosoftSQL: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsNetworkDrive: string; readonly connectorsOracle: string; readonly connectorsPostgreSQL: string; readonly connectorsS3: string; readonly connectorsWorkplaceSearch: string; readonly crawlerExtractionRules: string; readonly crawlerManaging: string; readonly crawlerOverview: string; readonly deployTrainedModels: string; readonly documentLevelSecurity: string; readonly elser: string; readonly engines: string; readonly ingestionApis: string; readonly ingestPipelines: string; readonly languageAnalyzers: string; readonly languageClients: string; readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; readonly mlDocumentEnrichment: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; readonly usersAccess: string; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false, diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 3aaf17560db47..3764d3e6efd5b 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/doc-links plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] --- import kbnDocLinksObj from './kbn_doc_links.devdocs.json'; diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index b1465811e2295..69318531901df 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/docs-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index ead5751fa35c3..3b0041e92a94f 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index 17263518fb4d1..6c7650530715b 100644 --- a/api_docs/kbn_ebt_tools.mdx +++ b/api_docs/kbn_ebt_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ebt-tools title: "@kbn/ebt-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ebt-tools plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index 6b880006797c0..ba5be3d9b2750 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index 0b8d0d939e4ae..c29d5b8dd2907 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index 2969a379e7d3c..5e7e203e9099c 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index eaf29c3077a8b..1b4b48beebca7 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-archiver plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] --- import kbnEsArchiverObj from './kbn_es_archiver.devdocs.json'; diff --git a/api_docs/kbn_es_errors.mdx b/api_docs/kbn_es_errors.mdx index ab41505be21e9..58bb73e76d2ec 100644 --- a/api_docs/kbn_es_errors.mdx +++ b/api_docs/kbn_es_errors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-errors title: "@kbn/es-errors" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-errors plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 9dac7b564cfc4..1e36c23a4ae00 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-query plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 9d1a75a856f82..958d349ebcbf8 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index cd2e3feaf7128..6a3f9acac5186 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/eslint-plugin-imports plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index d6f3aa5be6f6b..bbe08f3124717 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 4d30f7635f93f..7535845c13c1f 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/field-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] --- import kbnFieldTypesObj from './kbn_field_types.devdocs.json'; diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 602d4b76e4b69..a9f638b69ffe3 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/find-used-node-modules plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] --- import kbnFindUsedNodeModulesObj from './kbn_find_used_node_modules.devdocs.json'; diff --git a/api_docs/kbn_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 512b1b3bf1c1c..e89bcb91ea00b 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index c0b9ff1ab00b0..27613f3c7e4b8 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index 3927471f08514..3969bb18a85a9 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_generate_csv_types.mdx b/api_docs/kbn_generate_csv_types.mdx index 0e35a01c9f842..296b6f32ef717 100644 --- a/api_docs/kbn_generate_csv_types.mdx +++ b/api_docs/kbn_generate_csv_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv-types title: "@kbn/generate-csv-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv-types'] --- import kbnGenerateCsvTypesObj from './kbn_generate_csv_types.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index fa3960f635cfa..5b5f25b8f9af4 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 52466ad20426f..b3a5386218da5 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/handlebars plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] --- import kbnHandlebarsObj from './kbn_handlebars.devdocs.json'; diff --git a/api_docs/kbn_hapi_mocks.mdx b/api_docs/kbn_hapi_mocks.mdx index cd8245459b82a..312114890ac9a 100644 --- a/api_docs/kbn_hapi_mocks.mdx +++ b/api_docs/kbn_hapi_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-hapi-mocks title: "@kbn/hapi-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/hapi-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index f6df58ece9340..a37d1ecb7a39e 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index 65d82c8aa5eb7..51e5023b97924 100644 --- a/api_docs/kbn_home_sample_data_card.mdx +++ b/api_docs/kbn_home_sample_data_card.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-card title: "@kbn/home-sample-data-card" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-card plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-card'] --- import kbnHomeSampleDataCardObj from './kbn_home_sample_data_card.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_tab.mdx b/api_docs/kbn_home_sample_data_tab.mdx index 139ab20896b0d..6cfc3ecb35995 100644 --- a/api_docs/kbn_home_sample_data_tab.mdx +++ b/api_docs/kbn_home_sample_data_tab.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-home-sample-data-tab title: "@kbn/home-sample-data-tab" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/home-sample-data-tab plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/home-sample-data-tab'] --- import kbnHomeSampleDataTabObj from './kbn_home_sample_data_tab.devdocs.json'; diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index c5d491f697f42..74290f7b26982 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 50c599f41ff0c..0ccb8e7051a6d 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index d6377597c1189..066367700c6c0 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/import-resolver plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] --- import kbnImportResolverObj from './kbn_import_resolver.devdocs.json'; diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 4d82a84859ef6..cd0d3ea9a262d 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/interpreter plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] --- import kbnInterpreterObj from './kbn_interpreter.devdocs.json'; diff --git a/api_docs/kbn_io_ts_utils.devdocs.json b/api_docs/kbn_io_ts_utils.devdocs.json index 9b5b0e6802254..40cd0884ecb9d 100644 --- a/api_docs/kbn_io_ts_utils.devdocs.json +++ b/api_docs/kbn_io_ts_utils.devdocs.json @@ -650,6 +650,31 @@ "deprecated": false, "trackAdoption": false, "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/io-ts-utils", + "id": "def-common.NonEmptyString", + "type": "Type", + "tags": [], + "label": "NonEmptyString", + "description": [], + "signature": [ + "string & ", + "Brand", + "<", + { + "pluginId": "@kbn/io-ts-utils", + "scope": "common", + "docId": "kibKbnIoTsUtilsPluginApi", + "section": "def-common.NonEmptyStringBrand", + "text": "NonEmptyStringBrand" + }, + ">" + ], + "path": "packages/kbn-io-ts-utils/src/non_empty_string_rt/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false } ], "objects": [ diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index d739403a3224c..eed88f2ab3ade 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/io-ts-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] --- import kbnIoTsUtilsObj from './kbn_io_ts_utils.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) for ques | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 38 | 0 | 38 | 4 | +| 39 | 0 | 39 | 4 | ## Common diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 479812a7adc8e..d4acef9dbda43 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/jest-serializers plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index 66f07ded66198..be4e1dfa7edf1 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index 9ba905d696133..04f04d6bc73d3 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 9b3835108da87..60bbd043f4e99 100644 --- a/api_docs/kbn_kibana_manifest_schema.mdx +++ b/api_docs/kbn_kibana_manifest_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-manifest-schema title: "@kbn/kibana-manifest-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/kibana-manifest-schema plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index 34e8d2903cb26..9ac9f4294c031 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 6c2e32868b8c7..2a8855f2702e0 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] --- import kbnLoggingObj from './kbn_logging.devdocs.json'; diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index c5fa54588432d..2a1b7e6dfd5d8 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/logging-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] --- import kbnLoggingMocksObj from './kbn_logging_mocks.devdocs.json'; diff --git a/api_docs/kbn_managed_vscode_config.mdx b/api_docs/kbn_managed_vscode_config.mdx index a0492d6bad71a..35fce2db8fb5d 100644 --- a/api_docs/kbn_managed_vscode_config.mdx +++ b/api_docs/kbn_managed_vscode_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-managed-vscode-config title: "@kbn/managed-vscode-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/managed-vscode-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/managed-vscode-config'] --- import kbnManagedVscodeConfigObj from './kbn_managed_vscode_config.devdocs.json'; diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 2cfeb2a59556c..fd94f66416a73 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/mapbox-gl plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] --- import kbnMapboxGlObj from './kbn_mapbox_gl.devdocs.json'; diff --git a/api_docs/kbn_ml_agg_utils.mdx b/api_docs/kbn_ml_agg_utils.mdx index 0faf79405aec0..bbb48df7e90d2 100644 --- a/api_docs/kbn_ml_agg_utils.mdx +++ b/api_docs/kbn_ml_agg_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-agg-utils title: "@kbn/ml-agg-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-agg-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-agg-utils'] --- import kbnMlAggUtilsObj from './kbn_ml_agg_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index 6d638b542f37f..3664a673b4ccb 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx index 39fbc8c7d9cc2..af897555cb6a4 100644 --- a/api_docs/kbn_ml_error_utils.mdx +++ b/api_docs/kbn_ml_error_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-error-utils title: "@kbn/ml-error-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-error-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] --- import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index 22473afe43b10..0d5eec908ea3f 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 6418b8eaa65f6..15741af3c7fd7 100644 --- a/api_docs/kbn_ml_is_populated_object.mdx +++ b/api_docs/kbn_ml_is_populated_object.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-populated-object title: "@kbn/ml-is-populated-object" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-populated-object plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-populated-object'] --- import kbnMlIsPopulatedObjectObj from './kbn_ml_is_populated_object.devdocs.json'; diff --git a/api_docs/kbn_ml_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index b6ab8338b1be1..64961e33e9aab 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index 20e06ed847ebb..a73f49add8a1c 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index e17712d9bddd9..0b45c5cd26d28 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index ceef878888760..35f0a8b02a6b6 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index e065a2d921874..587c8453128d1 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index 619f44aa95904..e1c844420142c 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index c338805fe37b6..51adc66a13288 100644 --- a/api_docs/kbn_ml_string_hash.mdx +++ b/api_docs/kbn_ml_string_hash.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-string-hash title: "@kbn/ml-string-hash" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-string-hash plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index 77df8b9fc96a9..f1eb955d2fb50 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index 271d8d798bb8b..21a68c43a88e9 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 0d286dc40dd46..dd405dac0663b 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/monaco plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index bf8dc9c9b2edb..f608b578c43e8 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index 109f0b15690e7..3497a15c009c8 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 2be790b6675bf..9d6ec4585eae9 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] --- import kbnOptimizerObj from './kbn_optimizer.devdocs.json'; diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index c03883615b27e..ae408b9a0c7eb 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index 39eafb2553e2a..50d4446c8492d 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index fe2a28edfb759..766ec15375419 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] --- import kbnPerformanceTestingDatasetExtractorObj from './kbn_performance_testing_dataset_extractor.devdocs.json'; diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index dfafc2995046a..8ea785d2a6232 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-generator plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] --- import kbnPluginGeneratorObj from './kbn_plugin_generator.devdocs.json'; diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 90bfd0727dc86..91024484857b2 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/plugin-helpers plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] --- import kbnPluginHelpersObj from './kbn_plugin_helpers.devdocs.json'; diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 894f6e1897cb7..2090a6ff74782 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/react-field plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index 9baafe7d47198..9898246c243f3 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index 0dcb15d07b2b0..f2350045bb743 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index 44df1cb8c7ba8..609ccdea58475 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index 3725fe5986da2..309b78834d066 100644 --- a/api_docs/kbn_repo_source_classifier.mdx +++ b/api_docs/kbn_repo_source_classifier.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-source-classifier title: "@kbn/repo-source-classifier" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-source-classifier plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 660528756fcdf..934a85e2f0a20 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index ee1fd9df0086e..991b131f66034 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 56b40b98c6111..db06808e4b24b 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rule-data-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index ac3326c810bdf..1f5facec95281 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index dbd27615a2886..8d753b443430d 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index 31bc6c9d9cbc9..6a7febcc12770 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index f51ac2903abc5..40bf71465317b 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index cbb7736cb66be..ae067bd381300 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 2d549e62753a9..226588554d715 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 966df1d43affc..5659087472cfd 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-es-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] --- import kbnSecuritysolutionEsUtilsObj from './kbn_securitysolution_es_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index ef163cd54d784..8825da7e86191 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_grouping.devdocs.json b/api_docs/kbn_securitysolution_grouping.devdocs.json index 710ef66fad8aa..d3c5052958e2d 100644 --- a/api_docs/kbn_securitysolution_grouping.devdocs.json +++ b/api_docs/kbn_securitysolution_grouping.devdocs.json @@ -29,7 +29,7 @@ "\nComposes grouping query and aggregations" ], "signature": [ - "({ additionalFilters, from, groupByFields, pageNumber, rootAggregations, runtimeMappings, size, sort, statsAggregations, to, }: ", + "({ additionalFilters, from, groupByFields, rootAggregations, runtimeMappings, size, pageNumber, sort, statsAggregations, to, }: ", "GroupingQueryArgs", ") => ", "GroupingQuery" @@ -43,7 +43,7 @@ "id": "def-common.getGroupingQuery.$1", "type": "Object", "tags": [], - "label": "{\n additionalFilters = [],\n from,\n groupByFields,\n pageNumber,\n rootAggregations,\n runtimeMappings,\n size = DEFAULT_GROUP_BY_FIELD_SIZE,\n sort,\n statsAggregations,\n to,\n}", + "label": "{\n additionalFilters = [],\n from,\n groupByFields,\n rootAggregations,\n runtimeMappings,\n size = DEFAULT_GROUP_BY_FIELD_SIZE,\n pageNumber,\n sort,\n statsAggregations,\n to,\n}", "description": [], "signature": [ "GroupingQueryArgs" @@ -69,7 +69,7 @@ "\nChecks if no group is selected" ], "signature": [ - "(groupKey: string | null) => boolean" + "(groupKeys: string[]) => boolean" ], "path": "packages/kbn-securitysolution-grouping/src/components/index.tsx", "deprecated": false, @@ -78,19 +78,19 @@ { "parentPluginId": "@kbn/securitysolution-grouping", "id": "def-common.isNoneGroup.$1", - "type": "CompoundType", + "type": "Array", "tags": [], - "label": "groupKey", + "label": "groupKeys", "description": [ - "selected group field value" + "selected group field values" ], "signature": [ - "string | null" + "string[]" ], "path": "packages/kbn-securitysolution-grouping/src/components/index.tsx", "deprecated": false, "trackAdoption": false, - "isRequired": false + "isRequired": true } ], "returnComment": [ @@ -108,7 +108,7 @@ "\nHook to configure grouping component" ], "signature": [ - "({ componentProps, defaultGroupingOptions, fields, groupingId, onGroupChange, tracker, }: GroupingArgs) => Grouping" + "({ componentProps, defaultGroupingOptions, fields, groupingId, maxGroupingLevels, onGroupChange, tracker, }: GroupingArgs) => Grouping" ], "path": "packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx", "deprecated": false, @@ -119,7 +119,7 @@ "id": "def-common.useGrouping.$1", "type": "Object", "tags": [], - "label": "{\n componentProps,\n defaultGroupingOptions,\n fields,\n groupingId,\n onGroupChange,\n tracker,\n}", + "label": "{\n componentProps,\n defaultGroupingOptions,\n fields,\n groupingId,\n maxGroupingLevels,\n onGroupChange,\n tracker,\n}", "description": [], "signature": [ "GroupingArgs" @@ -135,82 +135,6 @@ } ], "interfaces": [ - { - "parentPluginId": "@kbn/securitysolution-grouping", - "id": "def-common.GroupingAggregation", - "type": "Interface", - "tags": [], - "label": "GroupingAggregation", - "description": [ - "Defines the shape of the aggregation returned by Elasticsearch" - ], - "signature": [ - { - "pluginId": "@kbn/securitysolution-grouping", - "scope": "common", - "docId": "kibKbnSecuritysolutionGroupingPluginApi", - "section": "def-common.GroupingAggregation", - "text": "GroupingAggregation" - }, - "" - ], - "path": "packages/kbn-securitysolution-grouping/src/components/types.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "@kbn/securitysolution-grouping", - "id": "def-common.GroupingAggregation.groupByFields", - "type": "Object", - "tags": [], - "label": "groupByFields", - "description": [], - "signature": [ - "{ buckets?: ", - { - "pluginId": "@kbn/securitysolution-grouping", - "scope": "common", - "docId": "kibKbnSecuritysolutionGroupingPluginApi", - "section": "def-common.RawBucket", - "text": "RawBucket" - }, - "[] | undefined; } | undefined" - ], - "path": "packages/kbn-securitysolution-grouping/src/components/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/securitysolution-grouping", - "id": "def-common.GroupingAggregation.groupsCount", - "type": "Object", - "tags": [], - "label": "groupsCount", - "description": [], - "signature": [ - "{ value?: number | null | undefined; } | undefined" - ], - "path": "packages/kbn-securitysolution-grouping/src/components/types.ts", - "deprecated": false, - "trackAdoption": false - }, - { - "parentPluginId": "@kbn/securitysolution-grouping", - "id": "def-common.GroupingAggregation.unitsCount", - "type": "Object", - "tags": [], - "label": "unitsCount", - "description": [], - "signature": [ - "{ value?: number | null | undefined; } | undefined" - ], - "path": "packages/kbn-securitysolution-grouping/src/components/types.ts", - "deprecated": false, - "trackAdoption": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "@kbn/securitysolution-grouping", "id": "def-common.GroupOption", @@ -306,21 +230,59 @@ "misc": [ { "parentPluginId": "@kbn/securitysolution-grouping", - "id": "def-common.GroupingFieldTotalAggregation", + "id": "def-common.DynamicGroupingProps", "type": "Type", - "tags": [], - "label": "GroupingFieldTotalAggregation", - "description": [], + "tags": [ + "interface" + ], + "label": "DynamicGroupingProps", + "description": [ + "Type for dynamic grouping component props where T is the consumer `GroupingAggregation`" + ], "signature": [ - "{ [x: string]: { value?: number | null | undefined; buckets?: ", + "{ isLoading: boolean; data?: ", { "pluginId": "@kbn/securitysolution-grouping", "scope": "common", "docId": "kibKbnSecuritysolutionGroupingPluginApi", - "section": "def-common.RawBucket", - "text": "RawBucket" + "section": "def-common.GroupingAggregation", + "text": "GroupingAggregation" }, - "[] | undefined; }; }" + " | undefined; activePage: number; itemsPerPage: number; groupingLevel?: number | undefined; inspectButton?: JSX.Element | undefined; onChangeGroupsItemsPerPage?: ((size: number) => void) | undefined; onChangeGroupsPage?: ((index: number) => void) | undefined; renderChildComponent: (groupFilter: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]) => React.ReactElement>; onGroupClose: () => void; selectedGroup: string; takeActionItems: (groupFilters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[], groupNumber: number) => JSX.Element[]; }" + ], + "path": "packages/kbn-securitysolution-grouping/src/hooks/use_grouping.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-grouping", + "id": "def-common.GroupingAggregation", + "type": "Type", + "tags": [], + "label": "GroupingAggregation", + "description": [], + "signature": [ + "RootAggregation", + " & ", + "GroupingFieldTotalAggregation", + "" ], "path": "packages/kbn-securitysolution-grouping/src/components/types.ts", "deprecated": false, diff --git a/api_docs/kbn_securitysolution_grouping.mdx b/api_docs/kbn_securitysolution_grouping.mdx index 548914f17feb8..2b716679e092a 100644 --- a/api_docs/kbn_securitysolution_grouping.mdx +++ b/api_docs/kbn_securitysolution_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-grouping title: "@kbn/securitysolution-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-grouping plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-grouping'] --- import kbnSecuritysolutionGroupingObj from './kbn_securitysolution_grouping.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-threat-hunting-explore](https://github.com/orgs/elast | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 20 | 0 | 15 | 4 | +| 17 | 0 | 12 | 6 | ## Common diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 012e37a5de10c..e2bbd21ba5d00 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] --- import kbnSecuritysolutionHookUtilsObj from './kbn_securitysolution_hook_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index c19977b427358..b16aed40d059c 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 61c51d7af0af1..16ac8b89a1297 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 97859ef4b0ff6..4ad236560201d 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] --- import kbnSecuritysolutionIoTsTypesObj from './kbn_securitysolution_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 087b27374d68b..0a2855949754a 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] --- import kbnSecuritysolutionIoTsUtilsObj from './kbn_securitysolution_io_ts_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index aadca05334b92..652e9b2b4eee7 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-api plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 2d19d6111ef53..b9f07add0070b 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-constants plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] --- import kbnSecuritysolutionListConstantsObj from './kbn_securitysolution_list_constants.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index e5f63ce69a791..8f978e102091b 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 9fbc74f2281fa..6dae9754b092b 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-list-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] --- import kbnSecuritysolutionListUtilsObj from './kbn_securitysolution_list_utils.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index d99c895cfbb07..0c8dfd59d0756 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-rules plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] --- import kbnSecuritysolutionRulesObj from './kbn_securitysolution_rules.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index fc1a7126299da..4040df55dfbb5 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-t-grid plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] --- import kbnSecuritysolutionTGridObj from './kbn_securitysolution_t_grid.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 5b42ba79461cd..7e0d6a471876b 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] --- import kbnSecuritysolutionUtilsObj from './kbn_securitysolution_utils.devdocs.json'; diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index c538168844e59..32506057fb398 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-http-tools plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] --- import kbnServerHttpToolsObj from './kbn_server_http_tools.devdocs.json'; diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 7c066060f57b3..b046e0bf3a0b1 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/server-route-repository plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] --- import kbnServerRouteRepositoryObj from './kbn_server_route_repository.devdocs.json'; diff --git a/api_docs/kbn_shared_svg.mdx b/api_docs/kbn_shared_svg.mdx index 57b4b0757c666..72d7f5a5a936b 100644 --- a/api_docs/kbn_shared_svg.mdx +++ b/api_docs/kbn_shared_svg.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-svg title: "@kbn/shared-svg" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-svg plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 0e0a7de059854..027126e63d38b 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index cbe1bbc592105..1f698d252027e 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index fd754396fe38d..0cb0c6e120097 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx index 704bf0d55a588..4db2e15234b2c 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen-mocks title: "@kbn/shared-ux-button-exit-full-screen-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen-mocks'] --- import kbnSharedUxButtonExitFullScreenMocksObj from './kbn_shared_ux_button_exit_full_screen_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_toolbar.mdx b/api_docs/kbn_shared_ux_button_toolbar.mdx index d5cbbf1440008..dcf9d9bec380e 100644 --- a/api_docs/kbn_shared_ux_button_toolbar.mdx +++ b/api_docs/kbn_shared_ux_button_toolbar.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-toolbar title: "@kbn/shared-ux-button-toolbar" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-toolbar plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-toolbar'] --- import kbnSharedUxButtonToolbarObj from './kbn_shared_ux_button_toolbar.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data.mdx b/api_docs/kbn_shared_ux_card_no_data.mdx index 6e187d6a67d6d..f36cb7d427e59 100644 --- a/api_docs/kbn_shared_ux_card_no_data.mdx +++ b/api_docs/kbn_shared_ux_card_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data title: "@kbn/shared-ux-card-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data'] --- import kbnSharedUxCardNoDataObj from './kbn_shared_ux_card_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx index f1eafa0073b75..463a82c4199c4 100644 --- a/api_docs/kbn_shared_ux_card_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_card_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-card-no-data-mocks title: "@kbn/shared-ux-card-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-card-no-data-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-card-no-data-mocks'] --- import kbnSharedUxCardNoDataMocksObj from './kbn_shared_ux_card_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 345346e831748..a497f9b7b3b39 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index e242bd651fa0d..5a10a184e8612 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index 4e391b9d8ab27..487bdb38fb383 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 662c634769dd1..03da8f2f7777b 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index 1e22cbdd34b00..ab7439a55c766 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.devdocs.json b/api_docs/kbn_shared_ux_file_types.devdocs.json index de6c162159d55..bd0ba52bf411a 100644 --- a/api_docs/kbn_shared_ux_file_types.devdocs.json +++ b/api_docs/kbn_shared_ux_file_types.devdocs.json @@ -79,7 +79,7 @@ "\nFind a set of files given some filters.\n" ], "signature": [ - "(args: { kind?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", + "(args: { kind?: string | string[] | undefined; kindToExclude?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", { "pluginId": "@kbn/shared-ux-file-types", "scope": "common", @@ -119,7 +119,7 @@ "- File filters" ], "signature": [ - "{ kind?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", + "{ kind?: string | string[] | undefined; kindToExclude?: string | string[] | undefined; status?: string | string[] | undefined; extension?: string | string[] | undefined; name?: string | string[] | undefined; meta?: M | undefined; } & ", { "pluginId": "@kbn/shared-ux-file-types", "scope": "common", @@ -1301,6 +1301,22 @@ "path": "packages/shared-ux/file/types/index.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/shared-ux-file-types", + "id": "def-common.FileKindBrowser.managementUiActions", + "type": "Object", + "tags": [], + "label": "managementUiActions", + "description": [ + "\nAllowed actions that can be done in the File Management UI. If not provided, all actions are allowed\n" + ], + "signature": [ + "{ list?: { enabled: boolean; } | undefined; delete?: { enabled: boolean; reason?: string | undefined; } | undefined; } | undefined" + ], + "path": "packages/shared-ux/file/types/index.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index 43c10d7423ef7..426d6f89d33ea 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 70 | 0 | 9 | 0 | +| 71 | 0 | 9 | 0 | ## Common diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index 7a684a3ddf9b6..17278d3391172 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index 7a12cb245836a..90687689300cf 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index dc82b967602d2..45abcd789302e 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx index 18b157f050418..abad65bf4bc03 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app-mocks title: "@kbn/shared-ux-link-redirect-app-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app-mocks'] --- import kbnSharedUxLinkRedirectAppMocksObj from './kbn_shared_ux_link_redirect_app_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index 8d43b2bc2609d..582712bf883ad 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 6ad8500d4ffa1..3f11bf724f8ff 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index da16a3294719e..a73b2130810da 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] --- import kbnSharedUxPageAnalyticsNoDataObj from './kbn_shared_ux_page_analytics_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx index a79ab025d8f39..be46cd48c00e3 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data-mocks title: "@kbn/shared-ux-page-analytics-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-analytics-no-data-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data-mocks'] --- import kbnSharedUxPageAnalyticsNoDataMocksObj from './kbn_shared_ux_page_analytics_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 675d8ca547baa..9f218fb8b7711 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] --- import kbnSharedUxPageKibanaNoDataObj from './kbn_shared_ux_page_kibana_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx index 87063d4968ca9..d1fb5d6a6f424 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data-mocks title: "@kbn/shared-ux-page-kibana-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-no-data-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data-mocks'] --- import kbnSharedUxPageKibanaNoDataMocksObj from './kbn_shared_ux_page_kibana_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template.mdx b/api_docs/kbn_shared_ux_page_kibana_template.mdx index d83b54deb377f..6a03b3207fd5a 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template title: "@kbn/shared-ux-page-kibana-template" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template'] --- import kbnSharedUxPageKibanaTemplateObj from './kbn_shared_ux_page_kibana_template.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx index a915fe7ce8d2f..b0a172e51dcd4 100644 --- a/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_template_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-template-mocks title: "@kbn/shared-ux-page-kibana-template-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-kibana-template-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-template-mocks'] --- import kbnSharedUxPageKibanaTemplateMocksObj from './kbn_shared_ux_page_kibana_template_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data.mdx b/api_docs/kbn_shared_ux_page_no_data.mdx index d386725c0f96e..0f4655e1c7b2c 100644 --- a/api_docs/kbn_shared_ux_page_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_no_data.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data title: "@kbn/shared-ux-page-no-data" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data'] --- import kbnSharedUxPageNoDataObj from './kbn_shared_ux_page_no_data.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config.mdx b/api_docs/kbn_shared_ux_page_no_data_config.mdx index b609f61949e3a..5b97d695996a6 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config title: "@kbn/shared-ux-page-no-data-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config'] --- import kbnSharedUxPageNoDataConfigObj from './kbn_shared_ux_page_no_data_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx index 3b8a6aea768c4..d0c153561221c 100644 --- a/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_config_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-config-mocks title: "@kbn/shared-ux-page-no-data-config-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-config-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-config-mocks'] --- import kbnSharedUxPageNoDataConfigMocksObj from './kbn_shared_ux_page_no_data_config_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx index b549d3be8ce48..20d822602caac 100644 --- a/api_docs/kbn_shared_ux_page_no_data_mocks.mdx +++ b/api_docs/kbn_shared_ux_page_no_data_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-no-data-mocks title: "@kbn/shared-ux-page-no-data-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-no-data-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-no-data-mocks'] --- import kbnSharedUxPageNoDataMocksObj from './kbn_shared_ux_page_no_data_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_page_solution_nav.mdx b/api_docs/kbn_shared_ux_page_solution_nav.mdx index 57b7f77917dbf..f59ea8eee9b01 100644 --- a/api_docs/kbn_shared_ux_page_solution_nav.mdx +++ b/api_docs/kbn_shared_ux_page_solution_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-solution-nav title: "@kbn/shared-ux-page-solution-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-page-solution-nav plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-solution-nav'] --- import kbnSharedUxPageSolutionNavObj from './kbn_shared_ux_page_solution_nav.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index cde75adfcbd03..7c1a23d42acbb 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] --- import kbnSharedUxPromptNoDataViewsObj from './kbn_shared_ux_prompt_no_data_views.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx index 449ec14d0e89b..a3b987ec20f3f 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views-mocks title: "@kbn/shared-ux-prompt-no-data-views-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-no-data-views-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views-mocks'] --- import kbnSharedUxPromptNoDataViewsMocksObj from './kbn_shared_ux_prompt_no_data_views_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index 54e8a60fb3a71..1ead3393a1d1e 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index 64431ebf0a9f6..b85098cc1faf9 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router title: "@kbn/shared-ux-router" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index 8f903a6a4599b..1a2dfe73599e1 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-router-mocks title: "@kbn/shared-ux-router-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-router-mocks plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index aa8aeb5ed628b..cd277b50d00af 100644 --- a/api_docs/kbn_shared_ux_storybook_config.mdx +++ b/api_docs/kbn_shared_ux_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-config title: "@kbn/shared-ux-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-config'] --- import kbnSharedUxStorybookConfigObj from './kbn_shared_ux_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_mock.mdx b/api_docs/kbn_shared_ux_storybook_mock.mdx index 1f594a6b27503..12a97778fad87 100644 --- a/api_docs/kbn_shared_ux_storybook_mock.mdx +++ b/api_docs/kbn_shared_ux_storybook_mock.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook-mock title: "@kbn/shared-ux-storybook-mock" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-storybook-mock plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook-mock'] --- import kbnSharedUxStorybookMockObj from './kbn_shared_ux_storybook_mock.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index 35030e8f65b69..0c09f2472b024 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-utility plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index b86313f3a585f..b14e5c2841704 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index 8a4c14deb442e..714f8f1f61bda 100644 --- a/api_docs/kbn_some_dev_log.mdx +++ b/api_docs/kbn_some_dev_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-some-dev-log title: "@kbn/some-dev-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/some-dev-log plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 8a33d6033f83b..be0949d0ede01 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/std plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] --- import kbnStdObj from './kbn_std.devdocs.json'; diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 8bb33b6f9fc6c..3bc8c63d56fa1 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/stdio-dev-helpers plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] --- import kbnStdioDevHelpersObj from './kbn_stdio_dev_helpers.devdocs.json'; diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index 0dcbe8b65314a..576f6d4cd96da 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/storybook plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] --- import kbnStorybookObj from './kbn_storybook.devdocs.json'; diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 009ae2d00a260..1b27ea9251410 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/telemetry-tools plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 077c288dbbd06..69b0b63437380 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index ac6e29488cd2d..503316ebfede3 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-jest-helpers plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index 4b38e19d2c144..5c043b0c4a6c1 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 1e093e1c87a67..493ea7c87e5ed 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/tooling-log plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index ec139fac154dc..9f58d89939bd4 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index c335bc8053866..9cdd2a92c31ff 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/typed-react-router-config plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] --- import kbnTypedReactRouterConfigObj from './kbn_typed_react_router_config.devdocs.json'; diff --git a/api_docs/kbn_ui_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index e4f4818385008..06135cf53284d 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index cfc3a2c905dbb..e4fcda7eaf227 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 2306fe2c1523e..8918f46eeb6f0 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-theme plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_url_state.mdx b/api_docs/kbn_url_state.mdx index 1ecdc4cf5f6b8..57ddb467440df 100644 --- a/api_docs/kbn_url_state.mdx +++ b/api_docs/kbn_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-url-state title: "@kbn/url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/url-state plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/url-state'] --- import kbnUrlStateObj from './kbn_url_state.devdocs.json'; diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 9678a8f3d941e..05e8e2b467830 100644 --- a/api_docs/kbn_user_profile_components.mdx +++ b/api_docs/kbn_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-user-profile-components title: "@kbn/user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/user-profile-components plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/user-profile-components'] --- import kbnUserProfileComponentsObj from './kbn_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index 0847fe990eba3..e9bda657218a8 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] --- import kbnUtilityTypesObj from './kbn_utility_types.devdocs.json'; diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index bd7b6dc7e7ca0..1aec92203b008 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utility-types-jest plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] --- import kbnUtilityTypesJestObj from './kbn_utility_types_jest.devdocs.json'; diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index 3bb37ce9fb475..31a898abdcd9c 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/utils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] --- import kbnUtilsObj from './kbn_utils.devdocs.json'; diff --git a/api_docs/kbn_yarn_lock_validator.mdx b/api_docs/kbn_yarn_lock_validator.mdx index 89bec78fbe1d0..2f26f43d82140 100644 --- a/api_docs/kbn_yarn_lock_validator.mdx +++ b/api_docs/kbn_yarn_lock_validator.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-yarn-lock-validator title: "@kbn/yarn-lock-validator" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/yarn-lock-validator plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/yarn-lock-validator'] --- import kbnYarnLockValidatorObj from './kbn_yarn_lock_validator.devdocs.json'; diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index 35b6ab39c77a0..ed7132ef96976 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaOverview plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] --- import kibanaOverviewObj from './kibana_overview.devdocs.json'; diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index 277ae838dc455..2f572069ba16c 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaReact plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] --- import kibanaReactObj from './kibana_react.devdocs.json'; diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 9710a9b016f88..e7081fb0b123e 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github description: API docs for the kibanaUtils plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] --- import kibanaUtilsObj from './kibana_utils.devdocs.json'; diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index e9b23fb4642c8..3a47dfb8f8190 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github description: API docs for the kubernetesSecurity plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] --- import kubernetesSecurityObj from './kubernetes_security.devdocs.json'; diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 7e35673a3ef08..d5ba1063609b4 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github description: API docs for the lens plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] --- import lensObj from './lens.devdocs.json'; diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index 18e54115992df..3fb2d265e634f 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseApiGuard plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] --- import licenseApiGuardObj from './license_api_guard.devdocs.json'; diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index f791025b3e353..04136e7075bf2 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the licenseManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] --- import licenseManagementObj from './license_management.devdocs.json'; diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 348e30ab776d8..67c1ef29b5996 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github description: API docs for the licensing plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 7a4f5df515fb3..580fb0856897a 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github description: API docs for the lists plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] --- import listsObj from './lists.devdocs.json'; diff --git a/api_docs/management.mdx b/api_docs/management.mdx index e86efc700e7ea..927666bf772e0 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github description: API docs for the management plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] --- import managementObj from './management.devdocs.json'; diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index d5ef557d05116..ba6ae74332111 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github description: API docs for the maps plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] --- import mapsObj from './maps.devdocs.json'; diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 97034dddf8ac6..bd11ed47d4e37 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github description: API docs for the mapsEms plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index d977d47492384..bd43f592ea080 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github description: API docs for the ml plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 90c474ac78f1a..4ccdc03719357 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoring plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] --- import monitoringObj from './monitoring.devdocs.json'; diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index 8d7ae64cb77ba..55dddb743db0d 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the monitoringCollection plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] --- import monitoringCollectionObj from './monitoring_collection.devdocs.json'; diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index 59a2eae887fb5..4c01c76df4884 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github description: API docs for the navigation plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] --- import navigationObj from './navigation.devdocs.json'; diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index f1ec49c987768..9eb555cf9ed2a 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github description: API docs for the newsfeed plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index bf6aade55bc14..ea77584e99213 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 01cac0e1d423a..666a83cdf4e15 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github description: API docs for the observability plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 321906e5ba6ff..fccba2b5c522f 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 171effe7c5467..365e4ce0d265e 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github description: API docs for the osquery plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index e0ee93073348b..95a66f4fb67fa 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -7,7 +7,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory description: Directory of public APIs available through plugins or packages. -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- @@ -15,13 +15,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 597 | 493 | 37 | +| 598 | 494 | 37 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 69151 | 525 | 59683 | 1333 | +| 69223 | 526 | 59703 | 1336 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 259 | 8 | 254 | 26 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 36 | 1 | 32 | 2 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 39 | 0 | 24 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 606 | 1 | 585 | 42 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 609 | 1 | 588 | 42 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 43 | 0 | 43 | 110 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | Asset manager plugin for entity assets (inventory, topology, etc) | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 9 | 0 | 9 | 0 | @@ -65,7 +65,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 97 | 0 | 78 | 7 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 37 | 0 | 35 | 2 | | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | APIs used to assess the quality of data in Elasticsearch indexes | 2 | 0 | 0 | 0 | -| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds embeddables service to Kibana | 544 | 11 | 442 | 4 | +| | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds embeddables service to Kibana | 546 | 11 | 444 | 4 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Extends embeddable plugin with more functionality | 14 | 0 | 14 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides encryption and decryption utilities for saved objects containing sensitive information. | 51 | 0 | 44 | 0 | | | [@elastic/enterprise-search-frontend](https://github.com/orgs/elastic/teams/enterprise-search-frontend) | Adds dashboards for discovering and managing Enterprise Search products. | 7 | 0 | 7 | 0 | @@ -90,7 +90,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 235 | 0 | 99 | 2 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Index pattern fields and ambiguous values formatters | 288 | 26 | 249 | 3 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 62 | 0 | 62 | 2 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 215 | 0 | 10 | 5 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | File upload, download, sharing, and serving over HTTP implementation in Kibana. | 217 | 0 | 10 | 5 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Simple UI for managing files in Kibana | 2 | 1 | 2 | 0 | | | [@elastic/fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1109 | 3 | 1004 | 28 | | ftrApis | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -104,7 +104,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Image embeddable | 3 | 0 | 3 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 177 | 0 | 172 | 3 | -| | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 44 | 0 | 41 | 9 | +| | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions | 45 | 0 | 42 | 10 | | ingestPipelines | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | inputControlVis | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Input Control visualization to Kibana | 0 | 0 | 0 | 0 | | | [@elastic/kibana-presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | - | 123 | 2 | 96 | 4 | @@ -232,7 +232,8 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 24 | 0 | 24 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 129 | 3 | 127 | 17 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 6 | 0 | 4 | 4 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 20 | 0 | 13 | 5 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 21 | 0 | 14 | 5 | +| | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 64 | 1 | 15 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 0 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 7 | 0 | 7 | 1 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 4 | 0 | 4 | 0 | @@ -425,7 +426,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 61 | 0 | 1 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 47 | 0 | 40 | 0 | | | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 52 | 12 | 43 | 0 | -| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 38 | 0 | 38 | 4 | +| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 39 | 0 | 39 | 4 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 13 | 0 | 13 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 85 | 0 | 77 | 5 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 41 | 2 | 35 | 0 | @@ -474,7 +475,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 341 | 1 | 337 | 32 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 68 | 0 | 62 | 1 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 104 | 0 | 93 | 1 | -| | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 20 | 0 | 15 | 4 | +| | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 17 | 0 | 12 | 6 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 15 | 0 | 7 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 147 | 0 | 125 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 528 | 0 | 515 | 0 | @@ -502,7 +503,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 2 | 0 | 2 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 1 | 0 | 1 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 14 | 0 | 6 | 0 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 70 | 0 | 9 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 71 | 0 | 9 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 7 | 0 | 7 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 17 | 0 | 15 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 17 | 0 | 9 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bb5dd22da4a5d..d0383bd897a40 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github description: API docs for the presentationUtil plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index fa7c514839f1d..5fe59c8d1a5c8 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 052f465958e43..e611da2cbb249 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github description: API docs for the remoteClusters plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] --- import remoteClustersObj from './remote_clusters.devdocs.json'; diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index b3e643f23c824..488ef2fa16eea 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github description: API docs for the reporting plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] --- import reportingObj from './reporting.devdocs.json'; diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 3ad92d06484ef..8a51e3435ed0c 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github description: API docs for the rollup plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] --- import rollupObj from './rollup.devdocs.json'; diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 2230c7e475fbf..0281bc7f316c3 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github description: API docs for the ruleRegistry plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] --- import ruleRegistryObj from './rule_registry.devdocs.json'; diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index fbb641e9ec07b..3922bd0239938 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github description: API docs for the runtimeFields plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] --- import runtimeFieldsObj from './runtime_fields.devdocs.json'; diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 4775f7bab3e25..14d55ddb58f46 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjects plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] --- import savedObjectsObj from './saved_objects.devdocs.json'; diff --git a/api_docs/saved_objects_finder.mdx b/api_docs/saved_objects_finder.mdx index c5cc15bc31edd..2fcbcc7d88c0b 100644 --- a/api_docs/saved_objects_finder.mdx +++ b/api_docs/saved_objects_finder.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsFinder title: "savedObjectsFinder" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsFinder plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsFinder'] --- import savedObjectsFinderObj from './saved_objects_finder.devdocs.json'; diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 8ed4d9ce7ea8b..e5e861b210704 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsManagement plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] --- import savedObjectsManagementObj from './saved_objects_management.devdocs.json'; diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 28d4dfa912c94..f89a314475878 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTagging plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] --- import savedObjectsTaggingObj from './saved_objects_tagging.devdocs.json'; diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index c4e422b6d4bf5..56588f7a39e29 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github description: API docs for the savedObjectsTaggingOss plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] --- import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.devdocs.json'; diff --git a/api_docs/saved_search.mdx b/api_docs/saved_search.mdx index b41919c5f029f..ba64470873a2d 100644 --- a/api_docs/saved_search.mdx +++ b/api_docs/saved_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/savedSearch title: "savedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the savedSearch plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedSearch'] --- import savedSearchObj from './saved_search.devdocs.json'; diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index eee83610aebe7..2032cc95f5e46 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotMode plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] --- import screenshotModeObj from './screenshot_mode.devdocs.json'; diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index 8b105e90157ca..a0d34f1ce02df 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github description: API docs for the screenshotting plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] --- import screenshottingObj from './screenshotting.devdocs.json'; diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 6a3ef50e5d725..8f05b6b6c7022 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github description: API docs for the security plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] --- import securityObj from './security.devdocs.json'; diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 49cd868cf6aee..ff027a894fe00 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github description: API docs for the securitySolution plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] --- import securitySolutionObj from './security_solution.devdocs.json'; diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 89f21abe997f4..2f539f8601a3f 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github description: API docs for the sessionView plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] --- import sessionViewObj from './session_view.devdocs.json'; diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 41725af018c9e..8a5c1e3d3b3ee 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github description: API docs for the share plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] --- import shareObj from './share.devdocs.json'; diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index 582bfce7f3253..c4b821d22e374 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github description: API docs for the snapshotRestore plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] --- import snapshotRestoreObj from './snapshot_restore.devdocs.json'; diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 2467dff1cd464..4b5964d1329b2 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github description: API docs for the spaces plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] --- import spacesObj from './spaces.devdocs.json'; diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index f2fd4e7fff1d3..b5a783c0352c6 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github description: API docs for the stackAlerts plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 12ffc30623e60..76676c70e8167 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index ec588948cbe18..48a65ec9218bc 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github description: API docs for the taskManager plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] --- import taskManagerObj from './task_manager.devdocs.json'; diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 439a2431b4376..dbc433c4a6906 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetry plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] --- import telemetryObj from './telemetry.devdocs.json'; diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index a445edd5a138f..4a2d5ddcb3340 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionManager plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] --- import telemetryCollectionManagerObj from './telemetry_collection_manager.devdocs.json'; diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 29ee2d6178438..206cd595f282b 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryCollectionXpack plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] --- import telemetryCollectionXpackObj from './telemetry_collection_xpack.devdocs.json'; diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index fbe7face22441..7d83295bd815b 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github description: API docs for the telemetryManagementSection plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] --- import telemetryManagementSectionObj from './telemetry_management_section.devdocs.json'; diff --git a/api_docs/threat_intelligence.mdx b/api_docs/threat_intelligence.mdx index 5ca7b1d27e7d4..6e08d1f6c67f8 100644 --- a/api_docs/threat_intelligence.mdx +++ b/api_docs/threat_intelligence.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/threatIntelligence title: "threatIntelligence" image: https://source.unsplash.com/400x175/?github description: API docs for the threatIntelligence plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'threatIntelligence'] --- import threatIntelligenceObj from './threat_intelligence.devdocs.json'; diff --git a/api_docs/timelines.devdocs.json b/api_docs/timelines.devdocs.json index 2cc9a92f16706..4d5c557d27e35 100644 --- a/api_docs/timelines.devdocs.json +++ b/api_docs/timelines.devdocs.json @@ -4155,7 +4155,7 @@ "label": "EntityType", "description": [], "signature": [ - "\"alerts\" | \"sessions\" | \"events\"" + "\"alerts\" | \"events\" | \"sessions\"" ], "path": "x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts", "deprecated": false, diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 41df96ec361dd..13a6ab02db045 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github description: API docs for the timelines plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] --- import timelinesObj from './timelines.devdocs.json'; diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index e0a7c8eca1a70..195ea7e628c28 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github description: API docs for the transform plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 0dfa7fa50555b..c6ada20ba1469 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github description: API docs for the triggersActionsUi plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index ba54dfe3328c8..124cea9d3ee49 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActions plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] --- import uiActionsObj from './ui_actions.devdocs.json'; diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 0111328be34a0..4c7f1c13c386b 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github description: API docs for the uiActionsEnhanced plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] --- import uiActionsEnhancedObj from './ui_actions_enhanced.devdocs.json'; diff --git a/api_docs/unified_field_list.mdx b/api_docs/unified_field_list.mdx index 97fceac316e4d..b54f077ec640f 100644 --- a/api_docs/unified_field_list.mdx +++ b/api_docs/unified_field_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedFieldList title: "unifiedFieldList" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedFieldList plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index e0acc17411f17..3f72b6536df3b 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 9655a20a1dda9..dc4452def7050 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] --- import unifiedSearchObj from './unified_search.devdocs.json'; diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index f70714d1e5938..65aca0296752f 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedSearch.autocomplete plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] --- import unifiedSearchAutocompleteObj from './unified_search_autocomplete.devdocs.json'; diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index fb8870f2325d2..b6383facf7fcd 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github description: API docs for the urlForwarding plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] --- import urlForwardingObj from './url_forwarding.devdocs.json'; diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 569cd4f8baf09..24515c95a5c7a 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github description: API docs for the usageCollection plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] --- import usageCollectionObj from './usage_collection.devdocs.json'; diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index 71a2488190eb8..53918746384ba 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github description: API docs for the ux plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] --- import uxObj from './ux.devdocs.json'; diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index aa3747f8476f0..81ac0a71e3af8 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github description: API docs for the visDefaultEditor plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] --- import visDefaultEditorObj from './vis_default_editor.devdocs.json'; diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 24c99b2aa19f8..2ee372bc06670 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeGauge plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] --- import visTypeGaugeObj from './vis_type_gauge.devdocs.json'; diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index d7649559452e8..4d368c89743f0 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeHeatmap plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] --- import visTypeHeatmapObj from './vis_type_heatmap.devdocs.json'; diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index c493a75a57076..94e0c5d1dcb8b 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypePie plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] --- import visTypePieObj from './vis_type_pie.devdocs.json'; diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index b954ce0b719d2..17258d78b7ad4 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTable plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] --- import visTypeTableObj from './vis_type_table.devdocs.json'; diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index b98b43d726106..2cbe6e2b666d4 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimelion plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] --- import visTypeTimelionObj from './vis_type_timelion.devdocs.json'; diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 7a80a887ba186..517213167e2fd 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeTimeseries plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] --- import visTypeTimeseriesObj from './vis_type_timeseries.devdocs.json'; diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index b760568fac4e3..fad9653e3d3ea 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVega plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] --- import visTypeVegaObj from './vis_type_vega.devdocs.json'; diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index 27174661c8845..dfe878c269ab1 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeVislib plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] --- import visTypeVislibObj from './vis_type_vislib.devdocs.json'; diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index f64e0c6adbde7..e774540c6c793 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github description: API docs for the visTypeXy plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] --- import visTypeXyObj from './vis_type_xy.devdocs.json'; diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index a8c2abb735484..ea85d8d3f48ce 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github description: API docs for the visualizations plugin -date: 2023-04-24 +date: 2023-04-25 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; From 8d8c2aab81e0d97e7c0382118dcf30e9059fe48a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 25 Apr 2023 08:08:53 +0200 Subject: [PATCH 34/36] [Synthetics] Push cloud and deployment id to service (#155660) --- .../server/synthetics_service/service_api_client.test.ts | 8 +++++++- .../server/synthetics_service/service_api_client.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts index 2ed86eb91ae52..700f339516215 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts @@ -441,7 +441,11 @@ describe('callAPI', () => { manifestUrl: 'http://localhost:8080/api/manifest', tls: { certificate: 'test-certificate', key: 'test-key' } as any, }, - { isDev: true, stackVersion: '8.7.0' } as UptimeServerSetup + { + isDev: true, + stackVersion: '8.7.0', + cloud: { cloudId: 'test-id', deploymentId: 'deployment-id' }, + } as UptimeServerSetup ); apiClient.locations = testLocations; @@ -462,6 +466,8 @@ describe('callAPI', () => { stack_version: '8.7.0', license_level: 'trial', license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', + cloud_id: 'test-id', + deployment_id: 'deployment-id', }, headers: { 'x-kibana-version': '8.7.0', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index 4fea1d04b64e4..876f0270c20ce 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -236,6 +236,8 @@ export class ServiceAPIClient { is_edit: isEdit, license_level: license.type, license_issued_to: license.issued_to, + deployment_id: this.server.cloud?.deploymentId, + cloud_id: this.server.cloud?.cloudId, }, headers: authHeader, httpsAgent: this.getHttpsAgent(baseUrl), From 1ae38df15dc564c00916bcd1171c8c696b813613 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 25 Apr 2023 08:09:22 +0200 Subject: [PATCH 35/36] [Synthetics] Fix default date range on errors page (#155661) --- .../synthetics_date_picker.test.tsx | 20 ------------------- .../date_picker/synthetics_date_picker.tsx | 12 +---------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx index 6fae43af920e5..a78710dd9994e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx @@ -25,26 +25,6 @@ describe('SyntheticsDatePicker component', () => { expect(await findByText('Refresh')).toBeInTheDocument(); }); - it('uses shared date range state when there is no url date range state', async () => { - const customHistory = createMemoryHistory({ - initialEntries: ['/?dateRangeStart=now-24h&dateRangeEnd=now'], - }); - - jest.spyOn(customHistory, 'push'); - - const { findByText } = render(, { - history: customHistory, - core: startPlugins, - }); - - expect(await findByText('~ 30 minutes ago')).toBeInTheDocument(); - - expect(customHistory.push).toHaveBeenCalledWith({ - pathname: '/', - search: 'dateRangeEnd=now-15m&dateRangeStart=now-30m', - }); - }); - it('should use url date range even if shared date range is present', async () => { const customHistory = createMemoryHistory({ initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx index 82f2874f9ffa2..a5eaeacf6c7ed 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx @@ -7,7 +7,6 @@ import React, { useContext, useEffect } from 'react'; import { EuiSuperDatePicker } from '@elastic/eui'; -import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../../common/constants/synthetics/client_defaults'; import { useUrlParams } from '../../../hooks'; import { CLIENT_DEFAULTS } from '../../../../../../common/constants'; import { @@ -16,12 +15,6 @@ import { SyntheticsRefreshContext, } from '../../../contexts'; -const isSyntheticsDefaultDateRange = (dateRangeStart: string, dateRangeEnd: string) => { - const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS_SYNTHETICS; - - return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END; -}; - export const SyntheticsDatePicker = ({ fullWidth }: { fullWidth?: boolean }) => { const [getUrlParams, updateUrl] = useUrlParams(); const { commonlyUsedRanges } = useContext(SyntheticsSettingsContext); @@ -36,10 +29,7 @@ export const SyntheticsDatePicker = ({ fullWidth }: { fullWidth?: boolean }) => useEffect(() => { const { from, to } = sharedTimeState ?? {}; - // if it's synthetics default range, and we have shared state from kibana, let's use that - if (isSyntheticsDefaultDateRange(start, end) && (from !== start || to !== end)) { - updateUrl({ dateRangeStart: from, dateRangeEnd: to }); - } else if (from !== start || to !== end) { + if (from !== start || to !== end) { // if it's coming url. let's update shared state data?.query.timefilter.timefilter.setTime({ from: start, to: end }); } From 4e4f408a34f418053d5433eec8d59250816b0ba8 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 25 Apr 2023 08:11:19 +0200 Subject: [PATCH 36/36] [Synthetics] Fix next / prev test navigation (#155624) --- .../step_details_page/step_page_nav.tsx | 30 ++++++++------- .../server/queries/get_journey_details.ts | 38 +++++++++++-------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_page_nav.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_page_nav.tsx index 528e3c9d6aa1b..9bd25a715dff1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_page_nav.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_page_nav.tsx @@ -57,19 +57,23 @@ export const StepPageNavigation = ({ testRunPage }: { testRunPage?: boolean }) = checkGroupId: data?.details?.next?.checkGroup, }); - if (testRunPage && data?.details?.previous?.checkGroup && data?.details?.next?.checkGroup) { - prevHref = getTestRunDetailLink({ - basePath, - monitorId, - locationId: selectedLocation?.id, - checkGroup: data?.details?.previous?.checkGroup, - }); - nextHref = getTestRunDetailLink({ - basePath, - monitorId, - locationId: selectedLocation?.id, - checkGroup: data?.details?.next?.checkGroup, - }); + if (testRunPage) { + if (data?.details?.previous?.checkGroup) { + prevHref = getTestRunDetailLink({ + basePath, + monitorId, + locationId: selectedLocation?.id, + checkGroup: data?.details?.previous?.checkGroup, + }); + } + if (data?.details?.next?.checkGroup) { + nextHref = getTestRunDetailLink({ + basePath, + monitorId, + locationId: selectedLocation?.id, + checkGroup: data?.details?.next?.checkGroup, + }); + } } if (!startedAt) { diff --git a/x-pack/plugins/synthetics/server/queries/get_journey_details.ts b/x-pack/plugins/synthetics/server/queries/get_journey_details.ts index 025688af45242..551696614d749 100644 --- a/x-pack/plugins/synthetics/server/queries/get_journey_details.ts +++ b/x-pack/plugins/synthetics/server/queries/get_journey_details.ts @@ -64,6 +64,15 @@ export const getJourneyDetails: UMElasticsearchQueryFn< body: { query: { bool: { + must_not: [ + { + term: { + 'monitor.check_group': { + value: journeySource.monitor.check_group, + }, + }, + }, + ], filter: [ { term: { @@ -93,6 +102,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< ...baseSiblingParams.body, query: { bool: { + must_not: baseSiblingParams.body.query.bool.must_not, filter: [ ...baseSiblingParams.body.query.bool.filter, { @@ -114,6 +124,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< ...baseSiblingParams.body, query: { bool: { + must_not: baseSiblingParams.body.query.bool.must_not, filter: [ ...baseSiblingParams.body.query.bool.filter, { @@ -151,20 +162,6 @@ export const getJourneyDetails: UMElasticsearchQueryFn< ({ _source: summarySource }) => summarySource.synthetics?.type === 'heartbeat/summary' )?._source; - const previousInfo = previousJourney - ? { - checkGroup: previousJourney._source.monitor.check_group, - timestamp: previousJourney._source['@timestamp'], - } - : undefined; - - const nextInfo = nextJourney - ? { - checkGroup: nextJourney._source.monitor.check_group, - timestamp: nextJourney._source['@timestamp'], - } - : undefined; - return { timestamp: journeySource['@timestamp'], journey: { ...journeySource, _id: foundJourney._id }, @@ -175,10 +172,19 @@ export const getJourneyDetails: UMElasticsearchQueryFn< }, } : {}), - previous: previousInfo, - next: nextInfo, + previous: filterNextPrevJourney(journeySource.monitor.check_group, previousJourney?._source), + next: filterNextPrevJourney(journeySource.monitor.check_group, nextJourney?._source), }; } else { return null; } }; + +const filterNextPrevJourney = (checkGroup: string, pingSource: DocumentSource) => { + return pingSource && pingSource.monitor.check_group !== checkGroup + ? { + checkGroup: pingSource.monitor.check_group, + timestamp: pingSource['@timestamp'], + } + : undefined; +};